diff --git a/example/src/Client/Client.ts b/example/src/Client/Client.ts index a6b6023..ae43c04 100644 --- a/example/src/Client/Client.ts +++ b/example/src/Client/Client.ts @@ -1,9 +1,17 @@ import { credentials } from "@grpc/grpc-js"; import { LightStreamerClient } from "../../../src/models/lightstreamer"; import { BlockProcessor } from "./utils/BlockProcessor"; -import { AccountsManager } from "./utils/AccountsManager"; +import { AccountData, AccountsManager } from "./utils/AccountsManager"; import { BlockCache } from "./utils/BlockCache"; +import { + Transaction as NativeTransaction, + Note as NativeNote, + Asset, +} from "@ironfish/rust-nodejs"; +import { MerkleWitness, notesTree } from "./utils/MerkleTree"; +import { BufferMap } from "buffer-map"; + const client = new LightStreamerClient( process.env["WALLET_SERVER_HOST"] ?? "localhost:50051", credentials.createInsecure(), @@ -61,4 +69,102 @@ export class Client { this.blockProcessor.stop(); this.handleStop?.(); } + + /* + This function is meant as example as to how to create a transaction using the core ironfish rust codebase, instead of the sdk. + For an example of using the sdk, see the ironfish wallet + In this case, we are using our own ironfish-rust-nodejs bindings, but other languages could be used as well with + separate bindings to the underlying functions. + */ + public async createTransaction( + account: AccountData, + to: { publicAddress: string }, + sendAmount: bigint, + sendAssetId: Buffer, + fee: bigint, // fee is always in native asset, $IRON + memo: string, + ): Promise { + const transaction = new NativeTransaction(account.key.spendingKey, 1); + const amountsNeeded = this.buildAmountsNeeded(sendAssetId, sendAmount, fee); + + // fund the transaction and calculate the witnesses + for (const [assetId, amount] of amountsNeeded) { + const fundNotes = await this.fundTransaction(account, assetId, amount); + fundNotes.map(({ note, witness }) => transaction.spend(note, witness)); + + const sendNote = new NativeNote( + to.publicAddress, + sendAmount, + memo, + assetId, + account.key.publicAddress, + ); + transaction.output(sendNote); + } + // TODO mark notes as spent, not absolutely critical as syncer should catch + return transaction; + } + + private buildAmountsNeeded( + assetId: Buffer, + amount: bigint, + fee: bigint, + ): BufferMap { + // add fee + const amountsNeeded = new BufferMap(); + amountsNeeded.set(Asset.nativeId(), fee); + + // add spend + const currentAmount = amountsNeeded.get(assetId) ?? 0n; + amountsNeeded.set(assetId, currentAmount + amount); + + return amountsNeeded; + } + + private async fundTransaction( + from: AccountData, + assetId: Buffer, + amount: bigint, + ): Promise<{ note: NativeNote; witness: MerkleWitness }[]> { + let currentValue = 0n; + const notesToSpend: { note: NativeNote; witness: MerkleWitness }[] = []; + const notes = from.assets.get(assetId); + if (!notes) { + throw new Error("No notes found for asset: " + assetId.toString("hex")); + } + for (const note of notes.values()) { + if (currentValue >= amount) { + break; + } + if ( + !note.note.assetId().equals(assetId) || + note.spent === true || + !note.sequence || + !note.merkleIndex + ) { + continue; + } + const witness = await notesTree.witness(note.merkleIndex); + if (!witness) { + console.warn( + "Could not calculate witness for note: ", + note.note.hash().toString("hex"), + ); + continue; + } + currentValue += note.note.value(); + notesToSpend.push({ note: note.note, witness }); + } + if (currentValue < amount) { + throw new Error( + "Insufficient funds for asset: " + + assetId.toString("hex") + + " needed: " + + amount.toString() + + " have: " + + currentValue.toString(), + ); + } + return notesToSpend; + } } diff --git a/example/src/Client/utils/send.ts b/example/src/Client/utils/send.ts deleted file mode 100644 index 20c7eff..0000000 --- a/example/src/Client/utils/send.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { BufferMap } from "buffer-map"; - -import { - Transaction as NativeTransaction, - Note as NativeNote, - Asset, -} from "@ironfish/rust-nodejs"; -import { MerkleWitness, notesTree } from "./MerkleTree"; -import { AccountData } from "./AccountsManager"; - -/* - This function is meant as example as to how to create a transaction using the core ironfish rust codebase, instead of the sdk. - For an example of using the sdk, see the ironfish wallet - In this case, we are using our own ironfish-rust-nodejs bindings, but other languages could be used as well with - separate bindings to the underlying functions. -*/ -export async function createTransaction( - account: AccountData, - to: { publicAddress: string }, - sendAmount: bigint, - sendAssetId: Buffer, - fee: bigint, // fee is always in native asset, $IRON - memo: string, -): Promise { - const transaction = new NativeTransaction(account.key.spendingKey, 1); - const amountsNeeded = buildAmountsNeeded(sendAssetId, sendAmount, fee); - - // fund the transaction and calculate the witnesses - for (const [assetId, amount] of amountsNeeded) { - const fundNotes = await fundTransaction(account, assetId, amount); - fundNotes.map(({ note, witness }) => transaction.spend(note, witness)); - - const sendNote = new NativeNote( - to.publicAddress, - sendAmount, - memo, - assetId, - account.key.publicAddress, - ); - transaction.output(sendNote); - } - // TODO mark notes as spent, not absolutely critical as syncer should catch - return transaction; -} - -function buildAmountsNeeded( - assetId: Buffer, - amount: bigint, - fee: bigint, -): BufferMap { - // add fee - const amountsNeeded = new BufferMap(); - amountsNeeded.set(Asset.nativeId(), fee); - - // add spend - const currentAmount = amountsNeeded.get(assetId) ?? 0n; - amountsNeeded.set(assetId, currentAmount + amount); - - return amountsNeeded; -} - -async function fundTransaction( - from: AccountData, - assetId: Buffer, - amount: bigint, -): Promise<{ note: NativeNote; witness: MerkleWitness }[]> { - let currentValue = 0n; - const notesToSpend: { note: NativeNote; witness: MerkleWitness }[] = []; - const notes = from.assets.get(assetId); - if (!notes) { - throw new Error("No notes found for asset: " + assetId.toString("hex")); - } - for (const note of notes.values()) { - if (currentValue >= amount) { - break; - } - if ( - !note.note.assetId().equals(assetId) || - note.spent === true || - !note.sequence || - !note.merkleIndex - ) { - continue; - } - const witness = await notesTree.witness(note.merkleIndex); - if (!witness) { - console.warn( - "Could not calculate witness for note: ", - note.note.hash().toString("hex"), - ); - continue; - } - currentValue += note.note.value(); - notesToSpend.push({ note: note.note, witness }); - } - if (currentValue < amount) { - throw new Error( - "Insufficient funds for asset: " + - assetId.toString("hex") + - " needed: " + - amount.toString() + - " have: " + - currentValue.toString(), - ); - } - return notesToSpend; -} diff --git a/example/src/send.ts b/example/src/send.ts index 8a4af22..1d4c1da 100644 --- a/example/src/send.ts +++ b/example/src/send.ts @@ -1,5 +1,4 @@ import { Client } from "./Client/Client"; -import { createTransaction } from "./Client/utils/send"; import { generateKeyFromPrivateKey } from "@ironfish/rust-nodejs"; export async function send( @@ -20,7 +19,7 @@ export async function send( if (!account) { throw new Error(`Account not found for intput spending key`); } - const transaction = await createTransaction( + const transaction = await client.createTransaction( account, { publicAddress: toPublicAddress }, BigInt(amount),