Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/fcl/src/wallet-provider-spec/draft-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ An authorization service is expected to know the Account and the Key that will b
FCL will use the `method` provided to request an array of composite signature from authorization service (Wrapped in a `PollingResponse`).
The authorization service will be sent a `Signable`.
The service is expected to construct an encoded message to sign from `Signable.voucher`.
It then needs to hash the encoded message, and prepend a required [transaction domain tag](https://github.com/onflow/flow-js-sdk/blob/master/packages/sdk/src/encode/encode.js#L12-L13).
It then needs to hash the encoded message, and prepend a required [transaction domain tag](../../../sdk/src/encode/encode.ts#L12-L13).
Finally it signs the payload with the user/s keys, producing a signature.
This signature, as a HEX string, is sent back to FCL as part of the `CompositeSignature` which includes the user address and keyID in the data property of a `PollingResponse`.

Expand Down
2 changes: 1 addition & 1 deletion packages/fcl/src/wallet-provider-spec/draft-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ An authorization service is expected to know the Account and the Key that will b
FCL will use the `method` provided to request an array of composite signature from authorization service (Wrapped in a `PollingResponse`).
The authorization service will be sent a `Signable`.
The service is expected to construct an encoded message to sign from `Signable.voucher`.
It then needs to hash the encoded message, and prepend a required [transaction domain tag](https://github.com/onflow/flow-js-sdk/blob/master/packages/sdk/src/encode/encode.js#L12-L13).
It then needs to hash the encoded message, and prepend a required [transaction domain tag](../../../sdk/src/encode/encode.ts#L12-L13).
Finally it signs the payload with the user/s keys, producing a signature.
This signature, as a HEX string, is sent back to FCL as part of the `CompositeSignature` which includes the user address and keyID in the data property of a `PollingResponse`.

Expand Down
2 changes: 1 addition & 1 deletion packages/rlp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Buffer} from "buffer"

export {Buffer}

type EncodeInput =
export type EncodeInput =
| Buffer
| string
| number
Expand Down
10 changes: 5 additions & 5 deletions packages/sdk/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ const response = await sdk.send(await sdk.build([

- [Builders](./src/build)

- [`sdk.args` & `sdk.arg`](./src/build/build-arguments)
- [`sdk.args` & `sdk.arg`](./src/build/build-arguments.ts)
- [`sdk.atBlockHeight`](./src/build/build-at-block-height.js)
- [`sdk.atBlockId`](./src/build/build-at-block-id.js)
- [`sdk.authorizations` & `authorization`](./src/build/build-authorizations.js)
Expand All @@ -509,7 +509,7 @@ const response = await sdk.send(await sdk.build([
- [`sdk.getCollection`](./src/build/build-get-collection)
- [`sdk.getEvents`](./src/build/build-get-events.js)
- [`sdk.getEventsAtBlockHeightRange`](./src/build/build-get-events-at-block-height-range.js)
- [`sdk.getEventsAtBlockIds`](./src/build/build-get-events-at-block-ids)
- [`sdk.getEventsAtBlockIds`](./src/build/build-get-events-at-block-ids.js)
- [`sdk.getLatestBlock`](./src/build/build-get-latest-block.js)
- [`sdk.getTransactionStatus`](./src/build/build-get-transaction-status.js)
- [`sdk.getTransaction`](./src/build/build-get-transaction.js)
Expand All @@ -526,15 +526,15 @@ const response = await sdk.send(await sdk.build([
- [`sdk.validator`](./src/build/build-validator.js)

- [Resolvers](./src/resolve)
- [`sdk.resolveAccounts`](./src/resolve/resolve-accounts.js)
- [`sdk.resolveAccounts`](./src/resolve/resolve-accounts.ts)
- [`sdk.resolveArguments`](./src/resolve/resolve-arguments.js)
- [`sdk.resolveCadence`](./src/resolve/resolve-cadence.js)
- [`sdk.resolveFinalNormalization`](./src/resolve/resolve-final-normalization.js)
- [`sdk.resolveVoucherIntercept`](./src/resolve/resolve-voucher-intercept.js)
- [`sdk.resolveProposerSequenceNumber`](./src/resolve/resolve-proposer-sequence-number.js)
- [`sdk.resolveRefBlockId`](./src/resolve/resolve-ref-block-id.js)
- [`sdk.resolveSignatures`](./src/resolve/resolve-signatures.js)
- [`sdk.resolveSignatures`](./src/resolve/resolve-signatures.ts)
- [`sdk.resolveValidators`](./src/resolve/resolve-validators.js)

- [Other Utils](./src/)
- [`sdk.voucherToTxId`](./src/resolve/voucher.js)
- [`sdk.voucherToTxId`](./src/resolve/voucher.ts)
2 changes: 1 addition & 1 deletion packages/sdk/src/contract.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as root from "./sdk"
import * as decode from "./decode/decode.js"
import * as encode from "./encode/encode.js"
import * as encode from "./encode/encode"
import * as interaction from "./interaction/interaction"
import * as send from "./send/send.js"
import * as template from "@onflow/util-template"
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/src/encode/encode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
encodeTransactionPayload,
encodeTransactionEnvelope,
encodeTxIdFromVoucher,
} from "./encode.js"
import * as root from "./encode.js"
} from "./encode"
import * as root from "./encode"

it("export contract interface", () => {
expect(root).toStrictEqual(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,74 +1,73 @@
import {SHA3} from "sha3"
import {encode, Buffer} from "@onflow/rlp"
import {encode, Buffer, EncodeInput} from "@onflow/rlp"
import {sansPrefix} from "@onflow/util-address"

export const encodeTransactionPayload = tx =>
export const encodeTransactionPayload = (tx: ITx) =>
prependTransactionDomainTag(rlpEncode(preparePayload(tx)))
export const encodeTransactionEnvelope = tx =>
export const encodeTransactionEnvelope = (tx: ITx) =>
prependTransactionDomainTag(rlpEncode(prepareEnvelope(tx)))
export const encodeTxIdFromVoucher = voucher =>
export const encodeTxIdFromVoucher = (voucher: IVoucher) =>
sha3_256(rlpEncode(prepareVoucher(voucher)))

const rightPaddedHexBuffer = (value, pad) =>
Buffer.from(value.padEnd(pad * 2, 0), "hex")
const rightPaddedHexBuffer = (value: string, pad: number) =>
Buffer.from(value.padEnd(pad * 2, "0"), "hex")

const leftPaddedHexBuffer = (value, pad) =>
Buffer.from(value.padStart(pad * 2, 0), "hex")
const leftPaddedHexBuffer = (value: string, pad: number) =>
Buffer.from(value.padStart(pad * 2, "0"), "hex")

const TRANSACTION_DOMAIN_TAG = rightPaddedHexBuffer(
Buffer.from("FLOW-V0.0-transaction").toString("hex"),
32
).toString("hex")
const prependTransactionDomainTag = tx => TRANSACTION_DOMAIN_TAG + tx
const prependTransactionDomainTag = (tx: string) => TRANSACTION_DOMAIN_TAG + tx

const addressBuffer = addr => leftPaddedHexBuffer(addr, 8)
const addressBuffer = (addr: string) => leftPaddedHexBuffer(addr, 8)

const blockBuffer = block => leftPaddedHexBuffer(block, 32)
const blockBuffer = (block: string) => leftPaddedHexBuffer(block, 32)

const argumentToString = arg => Buffer.from(JSON.stringify(arg), "utf8")
const argumentToString = (arg: Record<string, any>) => Buffer.from(JSON.stringify(arg), "utf8")

const scriptBuffer = script => Buffer.from(script, "utf8")
const signatureBuffer = signature => Buffer.from(signature, "hex")
const scriptBuffer = (script: string) => Buffer.from(script, "utf8")
const signatureBuffer = (signature: string) => Buffer.from(signature, "hex")

const rlpEncode = v => {
const rlpEncode = (v: EncodeInput) => {
return encode(v).toString("hex")
}

const sha3_256 = msg => {
const sha3_256 = (msg: string) => {
const sha = new SHA3(256)
sha.update(Buffer.from(msg, "hex"))
return sha.digest().toString("hex")
}

const preparePayload = tx => {
const preparePayload = (tx: ITx) => {
validatePayload(tx)

return [
scriptBuffer(tx.cadence),
scriptBuffer(tx.cadence || ''),
tx.arguments.map(argumentToString),
blockBuffer(tx.refBlock),
blockBuffer(tx.refBlock || ''),
tx.computeLimit,
addressBuffer(sansPrefix(tx.proposalKey.address)),
addressBuffer(sansPrefix(tx.proposalKey.address || '')),
tx.proposalKey.keyId,
tx.proposalKey.sequenceNum,
addressBuffer(sansPrefix(tx.payer)),
tx.authorizers.map(authorizer => addressBuffer(sansPrefix(authorizer))),
]
}

const prepareEnvelope = tx => {
const prepareEnvelope = (tx: ITx) => {
validateEnvelope(tx)

return [preparePayload(tx), preparePayloadSignatures(tx)]
}

const preparePayloadSignatures = tx => {
const preparePayloadSignatures = (tx: ITx) => {
const signers = collectSigners(tx)

return tx.payloadSigs
.map(sig => {
return tx.payloadSigs?.map((sig: ISig) => {
return {
signerIndex: signers.get(sig.address),
signerIndex: signers.get(sig.address) || '',
keyId: sig.keyId,
sig: sig.sig,
}
Expand All @@ -79,45 +78,51 @@ const preparePayloadSignatures = tx => {

if (a.keyId > b.keyId) return 1
if (a.keyId < b.keyId) return -1

return 0
})
.map(sig => {
return [sig.signerIndex, sig.keyId, signatureBuffer(sig.sig)]
})
}

const collectSigners = tx => {
const signers = new Map()
const collectSigners = (tx: IVoucher | ITx) => {
const signers = new Map<string, number>()
let i = 0

const addSigner = addr => {
const addSigner = (addr: string) => {
if (!signers.has(addr)) {
signers.set(addr, i)
i++
}
}

addSigner(tx.proposalKey.address)
if (tx.proposalKey.address){
addSigner(tx.proposalKey.address)
}
addSigner(tx.payer)
tx.authorizers.forEach(addSigner)

return signers
}

const prepareVoucher = voucher => {
const prepareVoucher = (voucher: IVoucher) => {
validateVoucher(voucher)

const signers = collectSigners(voucher)

const prepareSigs = sigs => {
const prepareSigs = (sigs: ISig[]) => {
return sigs
.map(({address, keyId, sig}) => {
return {signerIndex: signers.get(address), keyId, sig}
.map(({ address, keyId, sig }) => {
return { signerIndex: signers.get(address) || '', keyId, sig }
})
.sort((a, b) => {
if (a.signerIndex > b.signerIndex) return 1
if (a.signerIndex < b.signerIndex) return -1
if (a.keyId > b.keyId) return 1
if (a.keyId < b.keyId) return -1

return 0
})
.map(sig => {
return [sig.signerIndex, sig.keyId, signatureBuffer(sig.sig)]
Expand All @@ -143,23 +148,23 @@ const prepareVoucher = voucher => {
]
}

const validatePayload = tx => {
const validatePayload = (tx: ITx) => {
payloadFields.forEach(field => checkField(tx, field))
proposalKeyFields.forEach(field =>
checkField(tx.proposalKey, field, "proposalKey")
)
}

const validateEnvelope = tx => {
const validateEnvelope = (tx: ITx) => {
payloadSigsFields.forEach(field => checkField(tx, field))
tx.payloadSigs.forEach((sig, index) => {
tx.payloadSigs?.forEach((sig, index) => {
payloadSigFields.forEach(field =>
checkField(sig, field, "payloadSigs", index)
)
})
}

const validateVoucher = voucher => {
const validateVoucher = (voucher: IVoucher) => {
payloadFields.forEach(field => checkField(voucher, field))
proposalKeyFields.forEach(field =>
checkField(voucher.proposalKey, field, "proposalKey")
Expand All @@ -178,12 +183,64 @@ const validateVoucher = voucher => {
})
}

const isNumber = v => typeof v === "number"
const isString = v => typeof v === "string"
const isObject = v => v !== null && typeof v === "object"
const isArray = v => isObject(v) && v instanceof Array
const isNumber = (v: any): v is number => typeof v === "number"
const isString = (v: any): v is string => typeof v === "string"
const isObject = (v: any) => v !== null && typeof v === "object"
const isArray = (v: any) => isObject(v) && v instanceof Array

interface IVoucherArgument {
type: string
value: string
}

interface IVoucherProposalKey {
address: string
keyId: number | null
sequenceNum: number | null
}

interface ISig {
address: string,
keyId: number,
sig: string,
}

export interface ITxProposalKey {
address?: string
keyId?: number
sequenceNum?: number
}
export interface ITx {
cadence: string | null;
refBlock: string | null;
computeLimit: string | null;
arguments: IVoucherArgument[]
proposalKey: ITxProposalKey
payer: string
authorizers: string[]
payloadSigs?: ISig[]
envelopeSigs?: ITxProposalKey[]
}

export interface IVoucher {
cadence: string;
refBlock: string;
computeLimit: number;
arguments: IVoucherArgument[]
proposalKey: IVoucherProposalKey
payer: string
authorizers: string[]
payloadSigs: ISig[]
envelopeSigs: ISig[]
}

interface IPayloadField {
name: string,
check: (v: any) => boolean,
defaultVal?: string
}

const payloadFields = [
const payloadFields: IPayloadField[] = [
{name: "cadence", check: isString},
{name: "arguments", check: isArray},
{name: "refBlock", check: isString, defaultVal: "0"},
Expand All @@ -193,42 +250,47 @@ const payloadFields = [
{name: "authorizers", check: isArray},
]

const proposalKeyFields = [
const proposalKeyFields: IPayloadField[] = [
{name: "address", check: isString},
{name: "keyId", check: isNumber},
{name: "sequenceNum", check: isNumber},
]

const payloadSigsFields = [{name: "payloadSigs", check: isArray}]
const payloadSigsFields: IPayloadField[] = [{name: "payloadSigs", check: isArray}]

const payloadSigFields = [
const payloadSigFields: IPayloadField[] = [
{name: "address", check: isString},
{name: "keyId", check: isNumber},
{name: "sig", check: isString},
]

const envelopeSigsFields = [{name: "envelopeSigs", check: isArray}]
const envelopeSigsFields: IPayloadField[] = [{ name: "envelopeSigs", check: isArray }]

const envelopeSigFields = [
const envelopeSigFields: IPayloadField[] = [
{name: "address", check: isString},
{name: "keyId", check: isNumber},
{name: "sig", check: isString},
]

const checkField = (obj, field, base, index) => {
const checkField = (
obj: Record<string, any>,
field: IPayloadField,
base?: string,
index?: number
) => {
const {name, check, defaultVal} = field
if (obj[name] == null && defaultVal != null) obj[name] = defaultVal
if (obj[name] == null) throw missingFieldError(name, base, index)
if (!check(obj[name])) throw invalidFieldError(name, base, index)
}

const printFieldName = (field, base, index) => {
const printFieldName = (field: string, base?: string, index?: number) => {
if (!!base)
return index == null ? `${base}.${field}` : `${base}.${index}.${field}`
return field
}

const missingFieldError = (field, base, index) =>
const missingFieldError = (field: string, base?: string, index?: number) =>
new Error(`Missing field ${printFieldName(field, base, index)}`)
const invalidFieldError = (field, base, index) =>
const invalidFieldError = (field: string, base?: string, index?: number) =>
new Error(`Invalid field ${printFieldName(field, base, index)}`)
Loading