Skip to content

Commit bfdae0e

Browse files
avcdsldGreg Santos
authored and
Greg Santos
committed
add txId calculation and preSendCheck feature for transaction sending
1 parent dc61f81 commit bfdae0e

12 files changed

+414
-7
lines changed

packages/sdk/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@onflow/util-address": "^0.0.0",
4444
"@onflow/util-invariant": "^0.0.0",
4545
"@onflow/util-template": "0.0.1",
46-
"deepmerge": "^4.2.2"
46+
"deepmerge": "^4.2.2",
47+
"sha3": "^2.1.4"
4748
}
4849
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {put} from "../interaction/interaction.js"
2+
3+
export function preSendCheck(fn) {
4+
return put('ix.pre-send-check', fn)
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {interaction} from "../interaction/interaction.js"
2+
import {preSendCheck} from "./build-pre-send-check"
3+
4+
describe("Build preSendCheck", () => {
5+
test("Build preSendCheck", async () => {
6+
const checkFunc = async () => "test func"
7+
8+
const ix = await preSendCheck(checkFunc)(interaction())
9+
10+
expect(ix.assigns["ix.pre-send-check"]).toEqual(checkFunc)
11+
})
12+
})

packages/sdk/src/encode/encode.js

+74-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { encode } from '@onflow/rlp';
1+
import { SHA3 } from "sha3"
2+
import { encode } from "@onflow/rlp"
3+
import { sansPrefix } from "@onflow/util-address"
24

35
export const encodeTransactionPayload = tx => prependTransactionDomainTag(rlpEncode(preparePayload(tx)))
46
export const encodeTransactionEnvelope = tx => prependTransactionDomainTag(rlpEncode(prepareEnvelope(tx)))
7+
export const encodeTxIdFromVoucher = voucher => sha3_256(rlpEncode(prepareVoucher(voucher)))
58

69
const rightPaddedHexBuffer = (value, pad) =>
710
Buffer.from(value.padEnd(pad * 2, 0), "hex")
@@ -25,6 +28,12 @@ const rlpEncode = v => {
2528
return encode(v).toString("hex")
2629
}
2730

31+
const sha3_256 = msg => {
32+
const sha = new SHA3(256)
33+
sha.update(Buffer.from(msg, "hex"))
34+
return sha.digest().toString("hex")
35+
}
36+
2837
const preparePayload = tx => {
2938
validatePayload(tx)
3039

@@ -88,6 +97,41 @@ const collectSigners = tx => {
8897
return signers
8998
}
9099

100+
const prepareVoucher = voucher => {
101+
validateVoucher(voucher)
102+
103+
const signers = collectSigners(voucher)
104+
105+
const prepareSigs = sigs => {
106+
return sigs.map(({ address, keyId, sig }) => {
107+
return { signerIndex: signers.get(address), keyId, sig }
108+
}).sort((a, b) => {
109+
if (a.signerIndex > b.signerIndex) return 1
110+
if (a.signerIndex < b.signerIndex) return -1
111+
if (a.keyId > b.keyId) return 1
112+
if (a.keyId < b.keyId) return -1
113+
}).map(sig => {
114+
return [sig.signerIndex, sig.keyId, signatureBuffer(sig.sig)]
115+
})
116+
}
117+
118+
return [
119+
[
120+
scriptBuffer(voucher.cadence),
121+
voucher.arguments.map(argumentToString),
122+
blockBuffer(voucher.refBlock),
123+
voucher.computeLimit,
124+
addressBuffer(sansPrefix(voucher.proposalKey.address)),
125+
voucher.proposalKey.keyId,
126+
voucher.proposalKey.sequenceNum,
127+
addressBuffer(sansPrefix(voucher.payer)),
128+
voucher.authorizers.map(authorizer => addressBuffer(sansPrefix(authorizer))),
129+
],
130+
prepareSigs(voucher.payloadSigs),
131+
prepareSigs(voucher.envelopeSigs),
132+
]
133+
}
134+
91135
const validatePayload = tx => {
92136
payloadFields.forEach(field => checkField(tx, field))
93137
proposalKeyFields.forEach(field =>
@@ -96,14 +140,33 @@ const validatePayload = tx => {
96140
}
97141

98142
const validateEnvelope = tx => {
99-
envelopeFields.forEach(field => checkField(tx, field))
143+
payloadSigsFields.forEach(field => checkField(tx, field))
100144
tx.payloadSigs.forEach((sig, index) => {
101145
payloadSigFields.forEach(field =>
102146
checkField(sig, field, "payloadSigs", index)
103147
)
104148
})
105149
}
106150

151+
const validateVoucher = voucher => {
152+
payloadFields.forEach(field => checkField(voucher, field))
153+
proposalKeyFields.forEach(field =>
154+
checkField(voucher.proposalKey, field, "proposalKey")
155+
)
156+
payloadSigsFields.forEach(field => checkField(voucher, field))
157+
voucher.payloadSigs.forEach((sig, index) => {
158+
payloadSigFields.forEach(field =>
159+
checkField(sig, field, "payloadSigs", index)
160+
)
161+
})
162+
envelopeSigsFields.forEach(field => checkField(voucher, field))
163+
voucher.envelopeSigs.forEach((sig, index) => {
164+
envelopeSigFields.forEach(field =>
165+
checkField(sig, field, "envelopeSigs", index)
166+
)
167+
})
168+
}
169+
107170
const isNumber = v => typeof v === "number"
108171
const isString = v => typeof v === "string"
109172
const isObject = v => v !== null && typeof v === "object"
@@ -125,14 +188,22 @@ const proposalKeyFields = [
125188
{name: "sequenceNum", check: isNumber},
126189
]
127190

128-
const envelopeFields = [{name: "payloadSigs", check: isArray}]
191+
const payloadSigsFields = [{name: "payloadSigs", check: isArray}]
129192

130193
const payloadSigFields = [
131194
{name: "address", check: isString},
132195
{name: "keyId", check: isNumber},
133196
{name: "sig", check: isString},
134197
]
135198

199+
const envelopeSigsFields = [{name: "envelopeSigs", check: isArray}]
200+
201+
const envelopeSigFields = [
202+
{name: "address", check: isString},
203+
{name: "keyId", check: isNumber},
204+
{name: "sig", check: isString},
205+
]
206+
136207
const checkField = (obj, field, base, index) => {
137208
const {name, check, defaultVal} = field
138209
if (obj[name] == null && defaultVal != null) obj[name] = defaultVal

packages/sdk/src/encode/encode.test.js

+193-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
const merge = require("deepmerge")
22

3-
import {encodeTransactionPayload, encodeTransactionEnvelope} from "./encode.js"
3+
import {encodeTransactionPayload, encodeTransactionEnvelope, encodeTxIdFromVoucher} from "./encode.js"
44
import * as root from "./encode.js"
55

66
it("export contract interface", () => {
77
expect(root).toStrictEqual(
88
expect.objectContaining({
99
encodeTransactionPayload: expect.any(Function),
1010
encodeTransactionEnvelope: expect.any(Function),
11+
encodeTxIdFromVoucher: expect.any(Function),
1112
})
1213
)
1314
})
@@ -241,3 +242,194 @@ describe("encode transaction", () => {
241242
})
242243
})
243244
})
245+
246+
const baseVoucher = {
247+
cadence: `transaction { execute { log("Hello, World!") } }`,
248+
arguments: [],
249+
refBlock: "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b",
250+
computeLimit: 42,
251+
proposalKey: {
252+
address: "01",
253+
keyId: 4,
254+
sequenceNum: 10,
255+
},
256+
payer: "01",
257+
authorizers: ["01"],
258+
payloadSigs: [
259+
{
260+
address: "01",
261+
keyId: 4,
262+
sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162",
263+
},
264+
],
265+
envelopeSigs: [],
266+
}
267+
268+
const buildVoucher = partialVoucher =>
269+
merge(baseVoucher, partialVoucher, {arrayMerge: combineMerge})
270+
271+
describe("encode txId from voucher", () => {
272+
const invalidPayloadCases = [
273+
["empty", {}],
274+
["non-object", "foo"],
275+
276+
["null cadence", buildVoucher({cadence: null})],
277+
["null computeLimit", buildVoucher({computeLimit: null})],
278+
["null proposalKey", buildVoucher({proposalKey: null})],
279+
["null proposalKey.address", buildVoucher({proposalKey: {address: null}})],
280+
["null proposalKey.keyId", buildVoucher({proposalKey: {keyId: null}})],
281+
[
282+
"null proposalKey.sequenceNum",
283+
buildVoucher({proposalKey: {sequenceNum: null}}),
284+
],
285+
["null payer", buildVoucher({payer: null})],
286+
["null authorizers", buildVoucher({authorizers: null})],
287+
288+
["non-string cadence", buildVoucher({cadence: 42})],
289+
["non-string refBlock", buildVoucher({refBlock: 42})],
290+
["non-number computeLimit", buildVoucher({computeLimit: "foo"})],
291+
["non-object proposalKey", buildVoucher({proposalKey: "foo"})],
292+
["non-string proposalKey.address", buildVoucher({proposalKey: {address: 42}})],
293+
["non-number proposalKey.keyId", buildVoucher({proposalKey: {keyId: "foo"}})],
294+
[
295+
"non-number proposalKey.sequenceNum",
296+
buildVoucher({proposalKey: {sequenceNum: "foo"}}),
297+
],
298+
["non-string payer", buildVoucher({payer: 42})],
299+
["non-array authorizers", buildVoucher({authorizers: {}})],
300+
]
301+
302+
const invalidPayloadSigsCases = [
303+
["null payloadSigs", buildVoucher({payloadSigs: null})],
304+
["null payloadSigs.0.address", buildVoucher({payloadSigs: [{address: null}]})],
305+
["null payloadSigs.0.keyId", buildVoucher({payloadSigs: [{keyId: null}]})],
306+
["null payloadSigs.0.sig", buildVoucher({payloadSigs: [{sig: null}]})],
307+
308+
["non-array payloadSigs", buildVoucher({payloadSigs: {}})],
309+
[
310+
"non-string payloadSigs.0.address",
311+
buildVoucher({payloadSigs: [{address: 42}]}),
312+
],
313+
[
314+
"non-number payloadSigs.0.keyId",
315+
buildVoucher({payloadSigs: [{keyId: "foo"}]}),
316+
],
317+
["non-string payloadSigs.0.sig", buildVoucher({payloadSigs: [{sig: 42}]})],
318+
]
319+
320+
const invalidEnvelopeSigsCases = [
321+
["null envelopeSigs", buildVoucher({envelopeSigs: null})],
322+
["null envelopeSigs.0.address", buildVoucher({envelopeSigs: [{address: null}]})],
323+
["null envelopeSigs.0.keyId", buildVoucher({envelopeSigs: [{keyId: null}]})],
324+
["null envelopeSigs.0.sig", buildVoucher({envelopeSigs: [{sig: null}]})],
325+
326+
["non-array envelopeSigs", buildVoucher({envelopeSigs: {}})],
327+
[
328+
"non-string envelopeSigs.0.address",
329+
buildVoucher({envelopeSigs: [{address: 42}]}),
330+
],
331+
[
332+
"non-number envelopeSigs.0.keyId",
333+
buildVoucher({envelopeSigs: [{keyId: "foo"}]}),
334+
],
335+
["non-string envelopeSigs.0.sig", buildVoucher({envelopeSigs: [{sig: 42}]})],
336+
]
337+
338+
// Test case format:
339+
// [
340+
// <test name>,
341+
// <tx obj>,
342+
// <tx id>, // These values are calculated using Flow Go SDK test code.
343+
// ]
344+
const validCases = [
345+
[
346+
"complete tx",
347+
buildVoucher({}),
348+
"118d6462f1c4182501d56f04a0cd23cf685283194bb316dceeb215b353120b2b"
349+
],
350+
[
351+
"complete tx with envelope sig",
352+
buildVoucher({envelopeSigs: [{ address: "01", keyId: 4, sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162"}]}),
353+
"363172029cc6bfc5df3f99e84393e82b6c11e2a920c0e8c3229d077ae8bc31f7"
354+
],
355+
[
356+
"empty cadence",
357+
buildVoucher({cadence: ""}),
358+
"41dbbb83852ec8aa84dbeff03e29c0ed9c4a17b374eb0aa81695d83ccb344faf",
359+
],
360+
[
361+
"null refBlock",
362+
buildVoucher({refBlock: null}),
363+
"b01ac14da3e2a64e4c2e0a341ae2da832ff366b4c18b665fdd1fb5837e6128e0",
364+
],
365+
[
366+
"zero computeLimit",
367+
buildVoucher({computeLimit: 0}),
368+
"c149bf2077e174ccbf190f28eecda915f355333afb247f5c2ec44c2d041faf64",
369+
],
370+
[
371+
"zero proposalKey.key",
372+
buildVoucher({proposalKey: {keyId: 0}}),
373+
"1627bf4a626af55e0230b466b3828cb54822e53585f704e99a37abbbe6fbe51a",
374+
],
375+
[
376+
"zero proposalKey.sequenceNum",
377+
buildVoucher({proposalKey: {sequenceNum: 0}}),
378+
"3e9541ecee13b87a1c7be5e9ef0a00e4d48937a6f3df25167ffab2d4b4c846f4",
379+
],
380+
[
381+
"multiple authorizers",
382+
buildVoucher({authorizers: ["01", "02"]}),
383+
"6c4b45769cabadf30a103693195845ae633907f701cdcfa775bb830b6c80cb5b",
384+
],
385+
[
386+
"empty payloadSigs",
387+
buildVoucher({payloadSigs: []}),
388+
"c56b673a57fb94d546b9c30ac637d20021d04faf046e2cd5ea32591b7175794e"
389+
],
390+
[
391+
"out-of-order payloadSigs -- by signer",
392+
buildVoucher({
393+
authorizers: ["01", "02", "03"],
394+
payloadSigs: [
395+
{address: "03", keyId: 0, sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162"},
396+
{address: "01", keyId: 0, sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162"},
397+
{address: "02", keyId: 0, sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162"},
398+
],
399+
}),
400+
"b67576744b0d051ba81e4d6635a47b9c8973b3adc7410bcd213880d5094b264a"
401+
],
402+
[
403+
"out-of-order payloadSigs -- by key ID",
404+
buildVoucher({
405+
authorizers: ["01"],
406+
payloadSigs: [
407+
{address: "01", keyId: 2, sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162"},
408+
{address: "01", keyId: 0, sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162"},
409+
{address: "01", keyId: 1, sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162"},
410+
],
411+
}),
412+
"183a74ecc0782fbffc364cac0c404ee01144ae258841b806a6274259bfc7ef8b"
413+
],
414+
]
415+
416+
describe("invalid", () => {
417+
test.each(invalidPayloadCases)("%s", (_, voucher) => {
418+
expect(() => encodeTxIdFromVoucher(voucher)).toThrow()
419+
})
420+
421+
test.each(invalidPayloadSigsCases)("%s", (_, voucher) => {
422+
expect(() => encodeTxIdFromVoucher(voucher)).toThrow()
423+
})
424+
425+
test.each(invalidEnvelopeSigsCases)("%s", (_, voucher) => {
426+
expect(() => encodeTxIdFromVoucher(voucher)).toThrow()
427+
})
428+
})
429+
430+
describe("valid", () => {
431+
test.each(validCases)("%s", (_, voucher, expectedTxId) => {
432+
expect(encodeTxIdFromVoucher(voucher)).toBe(expectedTxId)
433+
})
434+
})
435+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {get, isFn} from "../interaction/interaction.js"
2+
import {createSignableVoucher} from "../resolve/voucher.js"
3+
4+
export async function resolvePreSendCheck(ix) {
5+
const fn = get(ix, "ix.pre-send-check")
6+
if (isFn(fn)) {
7+
await fn(createSignableVoucher(ix))
8+
}
9+
return ix
10+
}

0 commit comments

Comments
 (0)