diff --git a/bun.lockb b/bun.lockb index 6359782..0f86480 100755 Binary files a/bun.lockb and b/bun.lockb differ 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"); +};