From 8111d1cffc0f49e227bc52a72fb8242086d6305a Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 29 Aug 2023 11:18:06 -0700 Subject: [PATCH] feat: adds send command to example client --- example/package.json | 1 + example/src/Client/Client.ts | 4 + example/src/Client/utils/AccountsManager.ts | 28 +++-- example/src/Client/utils/merkle.ts | 5 +- example/src/Client/utils/send.ts | 125 ++++++++++++++++++++ example/src/index.ts | 41 +++++++ example/yarn.lock | 48 ++++++++ 7 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 example/src/Client/utils/send.ts diff --git a/example/package.json b/example/package.json index 4ec3d13..4930386 100644 --- a/example/package.json +++ b/example/package.json @@ -13,6 +13,7 @@ "dotenv": "^16.3.1", "leveldown": "^6.1.1", "levelup": "^5.1.1", + "@ironfish/rust-nodejs": "^1.7.0", "ts-node": "^10.9.1", "typescript": "^5.1.6" } diff --git a/example/src/Client/Client.ts b/example/src/Client/Client.ts index d1915c5..4787d9f 100644 --- a/example/src/Client/Client.ts +++ b/example/src/Client/Client.ts @@ -25,6 +25,10 @@ export class Client { this.accountsManager.addAccount(privateKey); } + public getAccount(publicKey: string) { + return this.accountsManager.getAccount(publicKey); + } + public async start() { await this.blockProcessor.start(); } diff --git a/example/src/Client/utils/AccountsManager.ts b/example/src/Client/utils/AccountsManager.ts index f6b5aeb..15523dc 100644 --- a/example/src/Client/utils/AccountsManager.ts +++ b/example/src/Client/utils/AccountsManager.ts @@ -17,18 +17,19 @@ export interface DecryptedNoteValue { spent: boolean; transactionHash: Buffer; index: number | null; + merkleIndex: number | null; nullifier: Buffer | null; blockHash: Buffer | null; sequence: number | null; } -interface AccountData { +export interface AccountData { key: Key; assets: Map< string, { balance: bigint; - decryptedNotes: DecryptedNoteValue[]; + decryptedNotes: Map; } >; } @@ -56,6 +57,10 @@ export class AccountsManager { }); } + public getAccount(publicKey: string) { + return this.accounts.get(publicKey); + } + public getPublicAddresses() { return Array.from(this.accounts.keys()); } @@ -73,14 +78,21 @@ export class AccountsManager { private _processBlockForTransactions(block: Buffer) { const parsedBlock = LightBlock.decode(block); + const blockNotes = parsedBlock.transactions.reduce( + (acc, tx) => acc + tx.outputs.length, + 0, + ); parsedBlock.transactions.forEach((tx) => { tx.outputs.forEach((output, index) => { - this._processNote( + // @todo: is this the correct index, or off by 1? Current +1 is for miners fee + const merkleIndex = parsedBlock.noteSize - blockNotes + index + 1; + return this._processNote( new NoteEncrypted(output.note), parsedBlock, tx, index, + merkleIndex, ); }); @@ -93,6 +105,7 @@ export class AccountsManager { block: LightBlock, tx: LightTransaction, index: number, + merkleIndex: number, ) { for (const publicKey of this.accounts.keys()) { // Get account data for public key @@ -117,19 +130,20 @@ export class AccountsManager { if (!account.assets.has(assetId)) { account.assets.set(assetId, { balance: BigInt(0), - decryptedNotes: [], + decryptedNotes: new Map(), }); } const assetEntry = account.assets.get(assetId)!; // Register note - assetEntry.decryptedNotes.push({ + assetEntry.decryptedNotes.set(foundNode.hash().toString("hex"), { accountId: publicKey, note: foundNode, spent: false, transactionHash: tx.hash, index, + merkleIndex, nullifier: null, // @todo: Get nullifier blockHash: block.hash, sequence: block.sequence, @@ -140,9 +154,9 @@ export class AccountsManager { assetEntry.balance = currentBalance + amount; logThrottled( - `Account ${publicKey} has ${assetEntry.decryptedNotes.length} notes for asset ${assetId}`, + `Account ${publicKey} has ${assetEntry.decryptedNotes.size} notes for asset ${assetId}`, 10, - assetEntry.decryptedNotes.length, + assetEntry.decryptedNotes.size, ); } } diff --git a/example/src/Client/utils/merkle.ts b/example/src/Client/utils/merkle.ts index 6bf81da..363362e 100644 --- a/example/src/Client/utils/merkle.ts +++ b/example/src/Client/utils/merkle.ts @@ -5,11 +5,14 @@ import { createDB } from "@ironfish/sdk/build/src/storage/utils"; import { LeafEncoding } from "@ironfish/sdk/build/src/merkletree/database/leaves"; import { NodeEncoding } from "@ironfish/sdk/build/src/merkletree/database/nodes"; import { NoteEncrypted } from "@ironfish/sdk/build/src/primitives/noteEncrypted"; +import { Witness } from "@ironfish/sdk/build/src/merkletree/witness"; const db = createDB({ location: "./testdb" }); db.open(); -const notesTree = new MerkleTree({ +export type MerkleWitness = Witness; + +export const notesTree = new MerkleTree({ hasher: new NoteHasher(), leafIndexKeyEncoding: BUFFER_ENCODING, leafEncoding: new LeafEncoding(), diff --git a/example/src/Client/utils/send.ts b/example/src/Client/utils/send.ts new file mode 100644 index 0000000..9dfdc77 --- /dev/null +++ b/example/src/Client/utils/send.ts @@ -0,0 +1,125 @@ +import { + Transaction as NativeTransaction, + TRANSACTION_VERSION, + Note as NativeNote, + Asset, +} from "@ironfish/rust-nodejs"; +import { MerkleWitness, notesTree } from "./merkle"; +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, + TRANSACTION_VERSION, + ); + const amountsNeeded = buildAmountsNeeded(sendAssetId, sendAmount, fee); + + // fund the transaction and calculate the witnesses + for (const [assetId, amount] of amountsNeeded) { + const fundedAmount = await fundTransaction( + transaction, + account, + assetId, + amount, + ); + const sendNote = new NativeNote( + to.publicAddress, + amount, + memo, + assetId, + account.key.publicAddress, + ); + transaction.output(sendNote); + + // issue change if necessary + if (fundedAmount > amount) { + const changeNote = new NativeNote( + account.key.publicAddress, + fundedAmount - amount, + memo, + assetId, + account.key.publicAddress, + ); + transaction.output(changeNote); + } + } + // TODO mark notes as spent + return transaction; +} + +function buildAmountsNeeded( + assetId: Buffer, + amount: bigint, + fee: bigint, +): Map { + const amountsNeeded = new Map(); + amountsNeeded.set(Asset.nativeId(), fee); + + // add spend + const currentAmount = amountsNeeded.get(assetId) ?? 0n; + amountsNeeded.set(assetId, currentAmount + amount); + + return amountsNeeded; +} + +async function fundTransaction( + transaction: NativeTransaction, + from: AccountData, + assetId: Buffer, + amount: bigint, +): Promise { + let currentValue = 0n; + const notesToSpend: { note: NativeNote; witness: MerkleWitness }[] = []; + const notes = from.assets.get(assetId.toString("hex")); + if (!notes) { + throw new Error("No notes found for asset: " + assetId.toString("hex")); + } + for (const note of notes.decryptedNotes.values()) { + if (currentValue >= amount) { + break; + } + if ( + note.note.assetId() !== 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(); + transaction.spend(note.note, note); + 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 currentValue; +} diff --git a/example/src/index.ts b/example/src/index.ts index 59a72a8..0e35fd9 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -2,8 +2,20 @@ import { config } from "dotenv"; config(); import { Client } from "./Client/Client"; +import { createTransaction } from "./Client/utils/send"; +import { generateKeyFromPrivateKey } from "@ironfish/rust-nodejs"; async function main() { + const args = process.argv.slice(2); + const argMap: { [key: string]: string } = {}; + + // Parse command line arguments + const command = args[0]; + args.slice(1).forEach((arg) => { + const [key, value] = arg.split("="); + argMap[key] = value; + }); + const client = new Client(); await client.start(); @@ -15,6 +27,35 @@ async function main() { client.addAccount(spendingKey); + if (command === "send") { + // If "send" command is provided, then proceed with additional operations. + const { assetId, toPublicAddress, amount, memo = "", fee = 1 } = argMap; + + console.log(`Asset ID: ${assetId}`); + console.log(`To Public Address: ${toPublicAddress}`); + console.log(`Amount: ${amount}`); + console.log(`Memo: ${memo}`); + console.log(`Fee: ${fee}`); + + const key = generateKeyFromPrivateKey(spendingKey); + const account = await client.getAccount(key.publicAddress); + if (!account) { + throw new Error(`Account not found for intput spending key`); + } + const transaction = await createTransaction( + account, + { publicAddress: toPublicAddress }, + BigInt(amount), + Buffer.from(assetId, "hex"), + BigInt(fee), + memo, + ); + const posted = transaction.post(null, BigInt(fee)); + + console.log("Posted transaction:"); + console.log(posted.toString("hex")); + } + await client.waitUntilClose(); } diff --git a/example/yarn.lock b/example/yarn.lock index f7fc20f..f7069f6 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -9,6 +9,54 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@ironfish/rust-nodejs-darwin-arm64@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@ironfish/rust-nodejs-darwin-arm64/-/rust-nodejs-darwin-arm64-1.7.0.tgz#e707ebee3785276d18676a554a8fe468757f9a54" + integrity sha512-PDh+pfAsjA1q9K9GPwXWPZ+Vf+Hu3AHCRNPFq0LJ35gvWg8/cVwmaBQo2505hMJe+LT03i5wn0f4J6N63b4lkA== + +"@ironfish/rust-nodejs-darwin-x64@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@ironfish/rust-nodejs-darwin-x64/-/rust-nodejs-darwin-x64-1.7.0.tgz#c6e589bd6e4d737865232b0ac2c4bd6198540082" + integrity sha512-fFfKTcsUxnqJPtYWxfJtRPOcFaziJw4SbLP2jfXemk7kp9ddoaYWJVqvoydVMe4cndDCz/OA/jLLFXn1Bm2MCw== + +"@ironfish/rust-nodejs-linux-arm64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@ironfish/rust-nodejs-linux-arm64-gnu/-/rust-nodejs-linux-arm64-gnu-1.7.0.tgz#5cf76485229c612f464ef4f49aa9c98b7b017ef8" + integrity sha512-LqJMDSO6MFPzF+EOAGdarIOJbCtkOL9kH1tpmMkhMbgfumpj+2Ma6jtDy6f3jX25jXW6YG3xV2oVWyApJuEgKw== + +"@ironfish/rust-nodejs-linux-arm64-musl@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@ironfish/rust-nodejs-linux-arm64-musl/-/rust-nodejs-linux-arm64-musl-1.7.0.tgz#ffc88fc9139eaf39a5217a35529db3fa96d0c0cf" + integrity sha512-ImsA9NJZQqPwSmeCDmk3kA66ccriOGEz/acljmwTgSvDYNea4RUJHwfMqn3fPV64iSGBgyH6NnVjtRwxjinaXQ== + +"@ironfish/rust-nodejs-linux-x64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@ironfish/rust-nodejs-linux-x64-gnu/-/rust-nodejs-linux-x64-gnu-1.7.0.tgz#a4102ec5fc5f913da2a3b78967238bb3ad7ccc96" + integrity sha512-L6BaYQzHcHSsmjL97rh+ie+V8elOMwBeOmTzvggyw92Doe/orxAmwJclKGFe8WBPpbMh4RZuiecBznm7lSqw3Q== + +"@ironfish/rust-nodejs-linux-x64-musl@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@ironfish/rust-nodejs-linux-x64-musl/-/rust-nodejs-linux-x64-musl-1.7.0.tgz#ed10bcd88db864f9c257895debe2ac6841578dc6" + integrity sha512-afDj8jAFdpTxYLXb4iv+3MpeR+TAKj9n/O46+1o8NxHWElgDgieJWmF/kC68nvGnxZY9ZxIibsPgfWxSNVHIQg== + +"@ironfish/rust-nodejs-win32-x64-msvc@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@ironfish/rust-nodejs-win32-x64-msvc/-/rust-nodejs-win32-x64-msvc-1.7.0.tgz#8cf4cf07299899adb4a8544d315738fe5d9914b2" + integrity sha512-6vpeIgJPCkTMpihARRiNQBAV/MDxakep7LJoBytOG0EbmJmzNqJHQwA3XcycXpLIpT2QrRsiHAtxK+O2VfuHjQ== + +"@ironfish/rust-nodejs@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@ironfish/rust-nodejs/-/rust-nodejs-1.7.0.tgz#6868812975839041d7579068df03e4663b6a8bb5" + integrity sha512-ELYdc2pzMCyY1lGO8ewanZy+BYahbllcUOV11fcN2XlpHD6NiA0W5v0lUqbSres4A76+GNKJqFjgB9V+XqTwdg== + optionalDependencies: + "@ironfish/rust-nodejs-darwin-arm64" "1.7.0" + "@ironfish/rust-nodejs-darwin-x64" "1.7.0" + "@ironfish/rust-nodejs-linux-arm64-gnu" "1.7.0" + "@ironfish/rust-nodejs-linux-arm64-musl" "1.7.0" + "@ironfish/rust-nodejs-linux-x64-gnu" "1.7.0" + "@ironfish/rust-nodejs-linux-x64-musl" "1.7.0" + "@ironfish/rust-nodejs-win32-x64-msvc" "1.7.0" + "@jridgewell/resolve-uri@^3.0.3": version "3.1.1" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"