Skip to content

Commit 1827b8c

Browse files
authored
Sign ledger transaction (#229)
1 parent 07cb0f1 commit 1827b8c

File tree

17 files changed

+814
-223
lines changed

17 files changed

+814
-223
lines changed

main/api/ironfish/Ironfish.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { logger } from "./logger";
1919
import packageJson from "../../../package.json";
2020
import { userSettingsStore } from "../../stores/userSettingsStore";
2121
import { SnapshotManager } from "../snapshot/snapshotManager";
22-
import { SplitPromise, splitPromise } from "../utils";
22+
import { SplitPromise, splitPromise } from "../utils/splitPromise";
2323

2424
export class Ironfish {
2525
public snapshotManager: SnapshotManager = new SnapshotManager();

main/api/ledger/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { observable } from "@trpc/server/observable";
22

33
import { ledgerManager, ConnectionStatus } from "./utils/ledger";
4+
import { handleSendTransactionInput } from "../transactions/handleSendTransaction";
45
import { t } from "../trpc";
56

67
export const ledgerRouter = t.router({
@@ -19,4 +20,9 @@ export const ledgerRouter = t.router({
1920
const result = await ledgerManager.importAccount();
2021
return result;
2122
}),
23+
submitLedgerTransaction: t.procedure
24+
.input(handleSendTransactionInput)
25+
.mutation(async (opts) => {
26+
return ledgerManager.submitTransaction(opts.input);
27+
}),
2228
});

main/api/ledger/utils/ledger.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import IronfishApp, {
99
ResponseViewKey,
1010
ResponseProofGenKey,
1111
} from "@zondax/ledger-ironfish";
12+
import { z } from "zod";
1213

1314
import { ledgerStore } from "../../../stores/ledgerStore";
14-
import { PromiseQueue } from "../../../utils/promiseQueue";
1515
import { handleImportAccount } from "../../accounts/handleImportAccount";
1616
import { logger } from "../../ironfish/logger";
17+
import { handleSendTransactionInput } from "../../transactions/handleSendTransaction";
18+
import { PromiseQueue } from "../../utils/promiseQueue";
19+
import { createUnsignedTransaction } from "../../utils/transactions";
1720

1821
export const DERIVATION_PATH = "m/44'/1338'/0";
1922
const IRONFISH_APP_NAME = "Ironfish";
@@ -354,6 +357,76 @@ class LedgerManager {
354357

355358
return returnValue;
356359
};
360+
361+
submitTransaction = async ({
362+
fromAccount,
363+
toAccount,
364+
assetId,
365+
amount,
366+
fee,
367+
memo,
368+
}: z.infer<typeof handleSendTransactionInput>) => {
369+
const returnValue = await this.taskQueue.enqueue(async () => {
370+
try {
371+
const unsignedTransaction = await createUnsignedTransaction({
372+
fromAccount,
373+
toAccount,
374+
assetId,
375+
amount,
376+
fee,
377+
memo,
378+
});
379+
const unsignedTransactionBuffer = Buffer.from(
380+
unsignedTransaction,
381+
"hex",
382+
);
383+
384+
if (unsignedTransactionBuffer.length > 16 * 1024) {
385+
throw new Error(
386+
"Transaction size is too large, must be less than 16kb.",
387+
);
388+
}
389+
390+
const connectResponse = await this.connect();
391+
392+
if (connectResponse.status !== "SUCCESS") {
393+
throw new Error(connectResponse.error.message);
394+
}
395+
396+
const transport = connectResponse.data;
397+
const ironfishAppReponse = await this.getIronfishApp(transport);
398+
399+
if (ironfishAppReponse.status !== "SUCCESS") {
400+
throw new Error(ironfishAppReponse.error.message);
401+
}
402+
403+
const signResponse = await ironfishAppReponse.data.sign(
404+
DERIVATION_PATH,
405+
unsignedTransactionBuffer,
406+
);
407+
408+
if (!signResponse.signature) {
409+
throw new Error(signResponse.errorMessage || "No signature returned");
410+
}
411+
412+
const splitSignParams = {
413+
unsignedTransaction: unsignedTransactionBuffer,
414+
signature: signResponse.signature.toString("hex"),
415+
};
416+
417+
// @todo: Sign and submit the transaction once addSignature is available from the RPC client
418+
return splitSignParams;
419+
} catch (err) {
420+
const message =
421+
err instanceof Error ? err.message : "Failed to import account";
422+
logger.error(message);
423+
await this.disconnect();
424+
throw new Error(message);
425+
}
426+
});
427+
428+
return returnValue;
429+
};
357430
}
358431

359432
export const ledgerManager = new LedgerManager();

main/api/snapshot/snapshotManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
getDefaultManifestUrl,
1010
} from "./utils";
1111
import { SnapshotUpdate } from "../../../shared/types";
12-
import { SplitPromise, splitPromise } from "../utils";
12+
import { SplitPromise, splitPromise } from "../utils/splitPromise";
1313

1414
export class SnapshotManager {
1515
onProgress: Event<[SnapshotUpdate]> = new Event();

main/api/transactions/handleSendTransaction.ts

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import {
2-
CreateTransactionRequest,
3-
CurrencyUtils,
4-
RawTransactionSerde,
5-
} from "@ironfish/sdk";
1+
import { RawTransactionSerde } from "@ironfish/sdk";
62
import { z } from "zod";
73

84
import { manager } from "../manager";
5+
import { createRawTransaction } from "../utils/transactions";
96

107
export const handleSendTransactionInput = z.object({
118
fromAccount: z.string(),
@@ -27,25 +24,14 @@ export async function handleSendTransaction({
2724
const ironfish = await manager.getIronfish();
2825
const rpcClient = await ironfish.rpcClient();
2926

30-
const params: CreateTransactionRequest = {
31-
account: fromAccount,
32-
outputs: [
33-
{
34-
publicAddress: toAccount,
35-
amount: CurrencyUtils.encode(BigInt(amount)),
36-
memo: memo ?? "",
37-
assetId: assetId,
38-
},
39-
],
40-
fee: CurrencyUtils.encode(BigInt(fee)),
41-
feeRate: null,
42-
expiration: undefined,
43-
confirmations: undefined,
44-
};
45-
46-
const createResponse = await rpcClient.wallet.createTransaction(params);
47-
const bytes = Buffer.from(createResponse.content.transaction, "hex");
48-
const rawTx = RawTransactionSerde.deserialize(bytes);
27+
const rawTx = await createRawTransaction({
28+
fromAccount,
29+
toAccount,
30+
assetId,
31+
amount,
32+
fee,
33+
memo,
34+
});
4935

5036
const postResponse = await rpcClient.wallet.postTransaction({
5137
transaction: RawTransactionSerde.serialize(rawTx).toString("hex"),
File renamed without changes.

main/api/utils/index.ts renamed to main/api/utils/splitPromise.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type SplitPromise<T> = {
55
resolve: PromiseResolve<T>;
66
reject: PromiseReject;
77
};
8+
89
export function splitPromise<T>(): SplitPromise<T> {
910
const [promise, resolve, reject] = PromiseUtils.split<T>();
1011
return { promise, resolve, reject };

main/api/utils/transactions.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
CreateTransactionRequest,
3+
CurrencyUtils,
4+
RawTransactionSerde,
5+
} from "@ironfish/sdk";
6+
import { z } from "zod";
7+
8+
import { manager } from "../manager";
9+
10+
export const createTransactionInput = z.object({
11+
fromAccount: z.string(),
12+
toAccount: z.string(),
13+
assetId: z.string(),
14+
amount: z.string(),
15+
fee: z.number(),
16+
memo: z.string().optional(),
17+
});
18+
19+
export type CreateTransactionInput = z.infer<typeof createTransactionInput>;
20+
21+
export async function createUnsignedTransaction({
22+
fromAccount,
23+
toAccount,
24+
assetId,
25+
amount,
26+
fee,
27+
memo,
28+
}: CreateTransactionInput) {
29+
const ironfish = await manager.getIronfish();
30+
const rpcClient = await ironfish.rpcClient();
31+
32+
const rawTx = await createRawTransaction({
33+
fromAccount,
34+
toAccount,
35+
assetId,
36+
amount,
37+
fee,
38+
memo,
39+
});
40+
41+
const serializedRawTx = RawTransactionSerde.serialize(rawTx);
42+
const builtTransactionResponse = await rpcClient.wallet.buildTransaction({
43+
rawTransaction: serializedRawTx.toString("hex"),
44+
account: fromAccount,
45+
});
46+
return builtTransactionResponse.content.unsignedTransaction;
47+
}
48+
49+
export async function createRawTransaction({
50+
fromAccount,
51+
toAccount,
52+
assetId,
53+
amount,
54+
fee,
55+
memo,
56+
}: CreateTransactionInput) {
57+
const ironfish = await manager.getIronfish();
58+
const rpcClient = await ironfish.rpcClient();
59+
60+
const params: CreateTransactionRequest = {
61+
account: fromAccount,
62+
outputs: [
63+
{
64+
publicAddress: toAccount,
65+
amount: CurrencyUtils.encode(BigInt(amount)),
66+
memo: memo ?? "",
67+
assetId: assetId,
68+
},
69+
],
70+
fee: CurrencyUtils.encode(BigInt(fee)),
71+
feeRate: null,
72+
expiration: undefined,
73+
confirmations: undefined,
74+
};
75+
76+
const createResponse = await rpcClient.wallet.createTransaction(params);
77+
const bytes = Buffer.from(createResponse.content.transaction, "hex");
78+
return RawTransactionSerde.deserialize(bytes);
79+
}

0 commit comments

Comments
 (0)