Skip to content

Commit

Permalink
PKG -- [sdk] TS voucher (#1803)
Browse files Browse the repository at this point in the history
* Nialexsan/add types (#1710)

* switch to uuid from @onflow/utils-uid

* create tsconfigs

* typedefs in ts

* types for actor util

* types for address util

* Revert "switch to uuid from @onflow/utils-uid"

This reverts commit 2a15ef5.

* PKG -- [util-actor] converted to ts

* updated tsconfig

* fixed ts types generation

* Resolve circular dependency

* ts rlp

* change type location

* more types

* build types during regular build

* fix tests

* VSN -- [root] Changeset

* Merge remote-tracking branch 'origin/master' into nialexsan/add-types

* Revert "Resolve circular dependency"

This reverts commit 36efc7d.

* update lock

* VSN -- [root] changeset

* Implement typescript for several branches & adjust configuration (#1750)

Implements typescript support for: @onflow/rlp, @onflow/util-uid, @onflow/util-template, @onflow/util-logger, @onflow/util-invariant, @onflow/util-encode-key, @onflow/util-address

* Convert @onflow/types to TS (#1760)

* [WIP] Convert @onflow/types to TS

* stash

* kind of working

* fix package.json

* fix dictionary

* fix tests

* stash

* strong type tests

* remove any

* rename

* changeset

* PKG -- [util-actor] Enhance TS support (#1761)

* PKG -- [util-actor] Enhance TS support

* rename handlerfnmap

* PKG -- [types] Simplify generics for @onflow/types (#1772)

* PKG -- [types] Simplify generics

* Fix array

* Fix Array

* PKG -- [config] Convert @onflow/config to TS (#1731)

* PKG -- [config] Add TypeScript

* Changeset

* fixup

* Remove unnecessary generic from util-actor

* remove non null assertions

---------

Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com>

* Fix JSDoc type generation (#1780)

* restore changeset

* align packages

* pre typescript

* any type for config

* update package lock

* PKG -- [util-encode-key] eslint ts config

* more ts

* move interaction types

* fix types

* more types

* authz types

* fix export

* fix imports

* fixed tests

* fix paths

* fix path

* fix some tests

* clean console log

* fix logic

* addressing PR comments

* fix build arguments

* shallow copy acct

* convert encode

* fix references

* revert changes

* ts resolve signature

* fix resolve accounts and interaction types

* fixed types

* fix path to interactions

* revert changes to changelogs

* fix types for resolve-accounts

* address comments

* changeset

* more descriptive names

* PKG -- [sdk] rename interfaces

* PKG -- [typedefs] fix merge conflict

* PKG -- [typedefs] fix merge conflicts

* PKG -- [sdk] fix interface imports

---------

Co-authored-by: Jordan Ribbink <jribbink@users.noreply.github.com>
  • Loading branch information
nialexsan and jribbink authored Nov 29, 2023
1 parent 0d09d83 commit f90ee4d
Show file tree
Hide file tree
Showing 20 changed files with 226 additions and 139 deletions.
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
8 changes: 4 additions & 4 deletions packages/sdk/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
164 changes: 113 additions & 51 deletions packages/sdk/src/encode/encode.js → packages/sdk/src/encode/encode.ts
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: Transaction) =>
prependTransactionDomainTag(rlpEncode(preparePayload(tx)))
export const encodeTransactionEnvelope = tx =>
export const encodeTransactionEnvelope = (tx: Transaction) =>
prependTransactionDomainTag(rlpEncode(prepareEnvelope(tx)))
export const encodeTxIdFromVoucher = voucher =>
export const encodeTxIdFromVoucher = (voucher: Voucher) =>
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: Transaction) => {
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: Transaction) => {
validateEnvelope(tx)

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

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

return tx.payloadSigs
.map(sig => {
return tx.payloadSigs?.map((sig: Sig) => {
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: Voucher | Transaction) => {
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: Voucher) => {
validateVoucher(voucher)

const signers = collectSigners(voucher)

const prepareSigs = sigs => {
const prepareSigs = (sigs: Sig[]) => {
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: Transaction) => {
payloadFields.forEach(field => checkField(tx, field))
proposalKeyFields.forEach(field =>
checkField(tx.proposalKey, field, "proposalKey")
)
}

const validateEnvelope = tx => {
const validateEnvelope = (tx: Transaction) => {
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: Voucher) => {
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 VoucherArgument {
type: string
value: string
}

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

interface Sig {
address: string,
keyId: number | string,
sig: string,
}

export interface TransactionProposalKey {
address?: string
keyId?: number | string
sequenceNum?: number
}
export interface Transaction {
cadence: string | null;
refBlock: string | null;
computeLimit: string | null;
arguments: VoucherArgument[]
proposalKey: TransactionProposalKey
payer: string
authorizers: string[]
payloadSigs?: Sig[]
envelopeSigs?: TransactionProposalKey[]
}

export interface Voucher {
cadence: string;
refBlock: string;
computeLimit: number;
arguments: VoucherArgument[]
proposalKey: VoucherProposalKey
payer: string
authorizers: string[]
payloadSigs: Sig[]
envelopeSigs: Sig[]
}

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

const payloadFields = [
const payloadFields: PayloadField[] = [
{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: PayloadField[] = [
{name: "address", check: isString},
{name: "keyId", check: isNumber},
{name: "sequenceNum", check: isNumber},
]

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

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

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

const envelopeSigFields = [
const envelopeSigFields: PayloadField[] = [
{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: PayloadField,
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

0 comments on commit f90ee4d

Please sign in to comment.