Skip to content

Commit

Permalink
feat: adds send command to example client
Browse files Browse the repository at this point in the history
  • Loading branch information
jowparks committed Sep 5, 2023
1 parent 7bcb611 commit 8111d1c
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 8 deletions.
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
4 changes: 4 additions & 0 deletions example/src/Client/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
28 changes: 21 additions & 7 deletions example/src/Client/utils/AccountsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, DecryptedNoteValue>;
}
>;
}
Expand Down Expand Up @@ -56,6 +57,10 @@ export class AccountsManager {
});
}

public getAccount(publicKey: string) {
return this.accounts.get(publicKey);
}

public getPublicAddresses() {
return Array.from(this.accounts.keys());
}
Expand All @@ -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,
);
});

Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
);
}
}
Expand Down
5 changes: 4 additions & 1 deletion example/src/Client/utils/merkle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NoteEncrypted, Buffer, Buffer, Buffer>;

export const notesTree = new MerkleTree({
hasher: new NoteHasher(),
leafIndexKeyEncoding: BUFFER_ENCODING,
leafEncoding: new LeafEncoding(),
Expand Down
125 changes: 125 additions & 0 deletions example/src/Client/utils/send.ts
Original file line number Diff line number Diff line change
@@ -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<NativeTransaction> {
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<Buffer, bigint> {
const amountsNeeded = new Map<Buffer, bigint>();
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<bigint> {
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;
}
41 changes: 41 additions & 0 deletions example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();
}

Expand Down
48 changes: 48 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,54 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"

"@ironfish/[email protected]":
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/[email protected]":
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/[email protected]":
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/[email protected]":
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/[email protected]":
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/[email protected]":
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/[email protected]":
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"
Expand Down

0 comments on commit 8111d1c

Please sign in to comment.