From 7c6725216abe1dddae327a30f0cae2e3fa56b9b1 Mon Sep 17 00:00:00 2001 From: Arno Simon Date: Thu, 14 Nov 2024 17:11:13 +0100 Subject: [PATCH] add signNobleTx and noble example --- bun.lockb | Bin 49457 -> 49489 bytes examples/noble.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/fireblocks.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 40 ++++++++++++++++++++++++++++++++- 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 examples/noble.ts diff --git a/bun.lockb b/bun.lockb index 635978202da1860777097b94283832a77ab6a889..0f86480f5af3897810e2507d8fa8e43a5f465624 100755 GIT binary patch delta 2377 zcmeH|Sx8h-9LDcxY7+*Tl1)lu2#u!MkZmN9QDA0ji!B-mEta-WVcOm-(PXREfiSCw zh=?Ksn-;B?mZu(l4Dzw}-dpJR-8q2|gVcjP`N40#v;5CF_ntHVa|fcS!_m~9I?wxo z`U^#&$&TZQDKH&UAjkR)T#|br*ZNFcakH%Fhz<7z(!QZm<* zc7n#81jU~MwVwegnXCUCPq@0o=k8Kifhg?$COnh!Ra=+qbd9Yib3MZ?(A>Q??k~K_ zJ>`20NQVb@pj;C@wEoDp%eBxG>;1T1`WZ-a^$)0GRs;j?7RRY5>IG=lSGN8dm*nan zv_6DOa>c&|CDccd{=pN;@MnMq|KIFPtnU=CunsT)l3Zyp?%A26)sWIQk)9LTslQaR ztkgcKUHuc;>1L(2WE`_oR}SeM{~Qd}7weXg&Vp_O9YX;~9EaPcyCu^^!aF<}W+d#N z`hmJ`i8l~8-bh~BTuOdZ94Yioqof<90k*(a&<)gRS|WwjdnoONeb5SRun9Ip6;#7I zP=+d7i=Y_h!ED$Ld}N&GW;l`^icz`wP-mIGg}|Xb!^x*bg=+=w$P`Cj%L0_QLPz!aS++POEVIh>mB4~$xcntTU z0#-sLyo4smgKtrO&=2z|y42Gg{~7(B;tlGR(J$kP1#~PX5Nq@8YT4ad&Kh&RCJixe*l?1k?>A@8d_9@@&Yamya4HkLeIW2; zDmfZUg^V#`6|994sPMWDl}Q7z-fIL^T&344R3;6=CJnHKs`XLlEvWvp#+a0I@nA|} zvl}h0dQsfRZ}sugGS~r{ZwA?7s`yq@Zp;cE5)iO+^^uhFZv&ZBY3lIIv`@epYCJjjZv zk{kxbUk1fr0r{_ite9&4hMO&@*4-qBrg)`0IQ^`KF&`t23+#FXc8p(jSPp=x z>Iz4o8}iT#{dOQ--7&z^aX0~k&;^H}4cegt)H+qIwGe|k*a$tK{xSt4b~arR7`5Ni zkv?U49n=;s!X-EjXF-idO-KE1AH-n`)I$SY0NtRkU>qhO2M=J(c4eyi)LgDYC+vr2 zXn~!u4YorQ48bJ4fXC1ZyI?oGg+AB-KQsJb^Y&||C0}}Lz9byW7F^$KS+>?TW(V`r f+0Ke^Oo?EFHk8}LvDo%pB7EOFC-Qf4pKE>tj0nHC diff --git a/examples/noble.ts b/examples/noble.ts new file mode 100644 index 0000000..17c1704 --- /dev/null +++ b/examples/noble.ts @@ -0,0 +1,56 @@ +import { Kiln, usdcToUusdc } from "../src/kiln"; +import fs from "node:fs"; +import 'dotenv/config' +import type { FireblocksIntegration } from "../src/fireblocks.ts"; + + +const apiSecret = fs.readFileSync(`${__dirname}/fireblocks_secret_prod.key`, 'utf8'); + +const k = new Kiln({ + baseUrl: process.env.KILN_API_URL as string, + apiToken: process.env.KILN_API_KEY as string, +}); + +const vault: FireblocksIntegration = { + provider: 'fireblocks', + fireblocksApiKey: process.env.FIREBLOCKS_API_KEY as string, + fireblocksSecretKey: apiSecret, + vaultId: 37 +}; + +try { + console.log('crafting...'); + // const s = await k.fireblocks.getSdk(vault); + // const p = await s.getPublicKeyInfoForVaultAccount({ + // assetId: "DYDX_DYDX", + // compressed: true, + // vaultAccountId: 37, + // change: 0, + // addressIndex: 0, + // }); + // console.log(getCosmosAddress('02d92b48d3e9ef34f2016eac7857a02768c88e30aea7a2366bc5ba032a22eceb8b', 'noble')); + const tx = await k.client.POST( + '/v1/noble/transaction/burn-usdc', + { + body: { + pubkey: '02d92b48d3e9ef34f2016eac7857a02768c88e30aea7a2366bc5ba032a22eceb8b', + recipient: '0xBC86717BaD3F8CcF86d2882a6bC351C94580A994', + amount_uusdc: usdcToUusdc('0.01').toString(), + } + } + ); + console.log('signing...'); + if(!tx.data?.data) throw new Error('No data in response'); + const signResponse = await k.fireblocks.signNobleTx(vault, tx.data.data); + console.log('broadcasting...'); + if(!signResponse.signed_tx?.data?.signed_tx_serialized) throw new Error('No signed_tx in response'); + const broadcastedTx = await k.client.POST("/v1/noble/transaction/broadcast", { + body: { + tx_serialized: signResponse.signed_tx.data.signed_tx_serialized, + } + }); + console.log(broadcastedTx); + +} catch (err) { + console.log(err); +} \ No newline at end of file diff --git a/package.json b/package.json index d76d1da..e1f6845 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "homepage": "https://github.com/kilnfi/sdk-js#readme", "dependencies": { "@types/bun": "^1.1.11", + "bech32": "^2.0.0", "fireblocks-sdk": "^5.32.0", "openapi-fetch": "^0.12.0", "viem": "^2.21.29" diff --git a/src/fireblocks.ts b/src/fireblocks.ts index ee8663c..d125855 100644 --- a/src/fireblocks.ts +++ b/src/fireblocks.ts @@ -453,6 +453,61 @@ export class FireblocksService { }; } + /** + * Sign a NOBLE transaction on Fireblocks + */ + async signNobleTx( + integration: FireblocksIntegration, + tx: components['schemas']['DYDXUnsignedTx'], + note?: string, + ): Promise<{ + signed_tx: { data: components['schemas']['DYDXSignedTx'] }; + fireblocks_tx: TransactionResponse; + }> { + const payload = { + rawMessageData: { + messages: [ + { + content: tx.unsigned_tx_hash, + derivationPath: [44, 118, integration.vaultId, 0, 0], + preHash: { + content: tx.unsigned_tx_serialized, + hashAlgorithm: 'SHA256', + }, + }, + ], + algorithm: SigningAlgorithm.MPC_ECDSA_SECP256K1, + }, + }; + + const fbSigner = this.getSigner(integration); + const fbNote = note ? note : 'NOBLE tx from @kilnfi/sdk'; + const fbTx = await fbSigner.sign(payload, undefined, fbNote); + const signature = fbTx.signedMessages?.[0]?.signature.fullSig; + + if (!signature) { + throw new Error('Fireblocks signature is missing'); + } + + const preparedTx = await this.client.POST('/v1/noble/transaction/prepare', { + body: { + pubkey: tx.pubkey, + tx_body: tx.tx_body, + tx_auth_info: tx.tx_auth_info, + signature: signature, + }, + }); + + if (preparedTx.error) { + throw new Error('Failed to prepare transaction'); + } + + return { + signed_tx: preparedTx.data, + fireblocks_tx: fbTx, + }; + } + /** * Sign a OSMO transaction on Fireblocks */ diff --git a/src/utils.ts b/src/utils.ts index 5a79741..0a9ccd5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ -import { formatUnits, parseUnits } from 'viem'; +import { formatUnits, parseUnits, ripemd160, sha256 } from "viem"; +import { bech32 } from "bech32"; /** * Convert wei to ETH @@ -161,9 +162,46 @@ export const uusdcToUsdc = (uusdc: bigint): string => { return formatUnits(uusdc, 6); }; +/** + * Convert USDC to uUSDC + */ +export const usdcToUusdc = (usdc: string): bigint => { + return parseUnits(usdc, 6); +}; + /** * Convert uKAVA to KAVA */ export const ukavaToKava = (ukava: bigint): string => { return formatUnits(ukava, 6); }; + + +/** + * Get a cosmos address from its public key and prefix + * @param pubkey + * @param prefix + */ +export const getCosmosAddress = (pubkey: string, prefix: string): string => { + const compressed_pubkey = compressPublicKey(pubkey); + const hash = sha256(Uint8Array.from(Buffer.from(compressed_pubkey, "hex"))); + const raw_addr = ripemd160(hash, "bytes") + return bech32.encode(prefix, bech32.toWords(raw_addr)); +}; + +/** + * Compress a cosmos public key + * @param pubkey + */ +export const compressPublicKey = (pubkey: string): string => { + const pub_key_buffer = new Uint8Array(Buffer.from(pubkey, "hex")); + if (pub_key_buffer.length !== 65) return pubkey; + const x = pub_key_buffer.slice(1, 33); + const y = pub_key_buffer.slice(33); + // We will add 0x02 if the last bit isn't set, otherwise we will add 0x03 + // @ts-ignore + const prefix = y[y.length - 1] & 1 ? "03" : "02"; + // Concatenate the prefix and the x value to get the compressed key + const compressed_key = Buffer.concat([new Uint8Array(Buffer.from(prefix, "hex")), x]); + return compressed_key.toString("hex"); +};