diff --git a/src/tx/builder/field-types/array.ts b/src/tx/builder/field-types/array.ts index ab6ae3dee1..ede6bb42e7 100644 --- a/src/tx/builder/field-types/array.ts +++ b/src/tx/builder/field-types/array.ts @@ -4,7 +4,7 @@ export default function genArrayField( deserialize: (value: Binary, params: unknown) => Output; }, ): { - serialize: (value: Input[], params: unknown) => Binary[]; + serialize: (value: readonly Input[], params: unknown) => Binary[]; deserialize: (value: Binary[], params: unknown) => Output[]; } { return { diff --git a/src/tx/builder/field-types/entry.ts b/src/tx/builder/field-types/entry.ts index 25ca0c614a..5507853ffe 100644 --- a/src/tx/builder/field-types/entry.ts +++ b/src/tx/builder/field-types/entry.ts @@ -7,14 +7,14 @@ import type { unpackTx as unpackTxType, buildTx as buildTxType } from '../index' export default function genEntryField(tag?: T): { serialize: ( - // TODO: replace with `Parameters>[0]`, + // TODO: replace with `TxParams & { tag: T }`, // but fix TS2502 value is referenced directly or indirectly in its own type annotation value: any, options: { buildTx: typeof buildTxType }, ) => Buffer; deserialize: ( value: Buffer, options: { unpackTx: typeof unpackTxType }, - // TODO: replace with `ReturnType>`, + // TODO: replace with `TxUnpacked & { tag: T }`, // TS2577 Return type annotation circularly references itself ) => any; } { diff --git a/src/tx/builder/field-types/index.ts b/src/tx/builder/field-types/index.ts index 02bc29746e..2f6d1d4ac5 100644 --- a/src/tx/builder/field-types/index.ts +++ b/src/tx/builder/field-types/index.ts @@ -12,6 +12,7 @@ import _fee from './fee'; import _field from './field'; import _gasLimit from './gas-limit'; import _gasPrice from './gas-price'; +import _map from './map'; import _mptree from './mptree'; import _name from './name'; import _nameFee from './name-fee'; @@ -24,6 +25,7 @@ import _shortUIntConst from './short-u-int-const'; import _string from './string'; import _ttl from './ttl'; import _uInt from './u-int'; +import _wrapped from './wrapped'; // TODO: remove after fixing https://github.com/Gerrit0/typedoc-plugin-missing-exports/issues/15 const abiVersion = _abiVersion; @@ -40,6 +42,7 @@ const fee = _fee; const field = _field; const gasLimit = _gasLimit; const gasPrice = _gasPrice; +const map = _map; const mptree = _mptree; const name = _name; const nameFee = _nameFee; @@ -52,6 +55,7 @@ const shortUIntConst = _shortUIntConst; const string = _string; const ttl = _ttl; const uInt = _uInt; +const wrapped = _wrapped; export type BinaryData = Buffer | Buffer[] | Buffer[][] | Array<[Buffer, Array<[Buffer, Buffer[]]>]>; @@ -77,6 +81,7 @@ export { field, gasLimit, gasPrice, + map, mptree, name, nameFee, @@ -89,4 +94,5 @@ export { string, ttl, uInt, + wrapped, }; diff --git a/src/tx/builder/field-types/map.ts b/src/tx/builder/field-types/map.ts new file mode 100644 index 0000000000..f1e4519700 --- /dev/null +++ b/src/tx/builder/field-types/map.ts @@ -0,0 +1,45 @@ +import { Tag } from '../constants'; +import { + encode, Encoding, Encoded, decode, +} from '../../../utils/encoder'; +import type { unpackTx as unpackTxType, buildTx as buildTxType } from '../index'; + +export default function genMapField(encoding: E, tag: T): { + serialize: ( + // TODO: replace with `TxParams & { tag: T }`, + // but fix TS2502 value is referenced directly or indirectly in its own type annotation + value: Record, any>, options: { buildTx: typeof buildTxType } + ) => Buffer; + deserialize: ( + value: Buffer, options: { unpackTx: typeof unpackTxType }, + // TODO: replace with `TxUnpacked & { tag: T }`, + // TS2577 Return type annotation circularly references itself + ) => Record, any>; + recursiveType: true; +} { + return { + serialize(object, { buildTx }) { + return decode(buildTx({ + tag: Tag.Mtree, + values: Object.entries(object).map(([key, value]) => ({ + tag: Tag.MtreeValue, + key: decode(key as Encoded.Generic), + value: decode(buildTx({ ...value as any, tag })), + })), + })); + }, + + deserialize(buffer, { unpackTx }) { + const { values } = unpackTx(encode(buffer, Encoding.Transaction), Tag.Mtree); + return Object.fromEntries(values + // TODO: remove after resolving https://github.com/aeternity/aeternity/issues/4066 + .filter(({ key }) => encoding !== Encoding.ContractAddress || key.length === 32) + .map(({ key, value }) => [ + encode(key, encoding), + unpackTx(encode(value, Encoding.Transaction), tag), + ])) as Record, any>; + }, + + recursiveType: true, + }; +} diff --git a/src/tx/builder/field-types/wrapped.ts b/src/tx/builder/field-types/wrapped.ts new file mode 100644 index 0000000000..373c13a98c --- /dev/null +++ b/src/tx/builder/field-types/wrapped.ts @@ -0,0 +1,32 @@ +import { Tag } from '../constants'; +import { encode, Encoding, decode } from '../../../utils/encoder'; +import type { unpackTx as unpackTxType, buildTx as buildTxType } from '../index'; + +type TagWrapping = Tag.AccountsMtree | Tag.CallsMtree | Tag.ChannelsMtree | Tag.ContractsMtree +| Tag.NameserviceMtree | Tag.OraclesMtree; + +export default function genWrappedField(tag: T): { + serialize: ( + // TODO: replace with `(TxParams & { tag: T })['payload']`, + // but fix TS2502 value is referenced directly or indirectly in its own type annotation + value: any, options: { buildTx: typeof buildTxType } + ) => Buffer; + deserialize: ( + value: Buffer, options: { unpackTx: typeof unpackTxType }, + // TODO: replace with `(TxUnpacked & { tag: T })['payload']`, + // TS2577 Return type annotation circularly references itself + ) => any; + recursiveType: true; +} { + return { + serialize(payload, { buildTx }) { + return decode(buildTx({ tag, payload })); + }, + + deserialize(buffer, { unpackTx }) { + return unpackTx(encode(buffer, Encoding.Transaction), tag).payload; + }, + + recursiveType: true, + }; +} diff --git a/src/tx/builder/index.ts b/src/tx/builder/index.ts index 7ebcb533bb..c8568dfbfb 100644 --- a/src/tx/builder/index.ts +++ b/src/tx/builder/index.ts @@ -93,7 +93,7 @@ export async function buildTxAsync(params: TxParamsAsync): Promise( - encodedTx: Encoded.Transaction | Encoded.Poi, + encodedTx: Encoded.Transaction | Encoded.Poi | Encoded.StateTrees | Encoded.CallStateTree, txType?: TxType, ): TxUnpacked & { tag: TxType } { const binary = rlpDecode(decode(encodedTx)); diff --git a/src/tx/builder/schema.ts b/src/tx/builder/schema.ts index a0c0a6db7d..61822b2f25 100644 --- a/src/tx/builder/schema.ts +++ b/src/tx/builder/schema.ts @@ -9,7 +9,7 @@ import SchemaTypes from './SchemaTypes'; import { uInt, shortUInt, coinAmount, name, nameId, nameFee, deposit, gasLimit, gasPrice, fee, address, pointers, entry, enumeration, mptree, shortUIntConst, string, encoded, raw, - array, boolean, ctVersion, abiVersion, ttl, nonce, + array, boolean, ctVersion, abiVersion, ttl, nonce, map, wrapped, } from './field-types'; import { Encoded, Encoding } from '../../utils/encoder'; import { idTagToEncoding } from './field-types/address'; @@ -43,21 +43,88 @@ interface EntryAny { recursiveType: true; } -interface EntryAnyArray { - serialize: (value: Array) => Buffer[]; - deserialize: (value: Buffer[]) => TxUnpacked[]; +const entryAny = entry() as unknown as EntryAny; + +interface EntryMtreeValueArray { + serialize: ( + value: Array, + ) => Buffer[]; + deserialize: (value: Buffer[]) => Array; recursiveType: true; } +const entryMtreeValueArray = array(entry(Tag.MtreeValue)) as unknown as EntryMtreeValueArray; + interface EntryTreesPoi { serialize: (value: TxParams & { tag: Tag.TreesPoi } | Uint8Array | Encoded.Transaction) => Buffer; deserialize: (value: Buffer) => TxUnpacked & { tag: Tag.TreesPoi }; recursiveType: true; } -const entryAny = entry() as unknown as EntryAny; const entryTreesPoi = entry(Tag.TreesPoi) as unknown as EntryTreesPoi; +interface MapContracts { + serialize: ( + value: Record, + ) => Buffer; + deserialize: ( + value: Buffer, + ) => Record; + recursiveType: true; +} + +const mapContracts = map(Encoding.ContractAddress, Tag.Contract) as unknown as MapContracts; + +interface MapAccounts { + serialize: ( + value: Record, + ) => Buffer; + deserialize: (value: Buffer) => Record; + recursiveType: true; +} + +const mapAccounts = map(Encoding.AccountAddress, Tag.Account) as unknown as MapAccounts; + +interface MapCalls { + serialize: ( + value: Record, + ) => Buffer; + deserialize: (value: Buffer) => Record; + recursiveType: true; +} + +const mapCalls = map(Encoding.Bytearray, Tag.ContractCall) as unknown as MapCalls; + +interface MapChannels { + serialize: ( + value: Record, + ) => Buffer; + deserialize: (value: Buffer) => Record; + recursiveType: true; +} + +const mapChannels = map(Encoding.Channel, Tag.Channel) as unknown as MapChannels; + +interface MapNames { + serialize: ( + value: Record, + ) => Buffer; + deserialize: (value: Buffer) => Record; + recursiveType: true; +} + +const mapNames = map(Encoding.Name, Tag.Name) as unknown as MapNames; + +interface MapOracles { + serialize: ( + value: Record, + ) => Buffer; + deserialize: (value: Buffer) => Record; + recursiveType: true; +} + +const mapOracles = map(Encoding.OracleAddress, Tag.Oracle) as unknown as MapOracles; + /** * @see {@link https://github.com/aeternity/protocol/blob/c007deeac4a01e401238412801ac7084ac72d60e/serializations.md#accounts-version-1-basic-accounts} */ @@ -182,18 +249,19 @@ export const txSchema = [{ callData: encoded(Encoding.ContractBytearray), }, { tag: shortUIntConst(Tag.ContractCall), - version: shortUIntConst(1, true), + version: shortUIntConst(2, true), callerId: address(Encoding.AccountAddress), callerNonce: shortUInt, height: shortUInt, contractId: address(Encoding.ContractAddress), - gasPrice, + // TODO: rename after resolving https://github.com/aeternity/protocol/issues/506 + gasPrice: uInt, gasUsed: shortUInt, returnValue: encoded(Encoding.ContractBytearray), returnType: enumeration(CallReturnType), // TODO: add serialization for // :: [ {
:: id, [ :: binary() }, :: binary() } ] - log: raw, + log: array(raw), }, { tag: shortUIntConst(Tag.Oracle), version: shortUIntConst(1, true), @@ -418,6 +486,7 @@ export const txSchema = [{ }, { tag: shortUIntConst(Tag.TreesPoi), version: shortUIntConst(1, true), + // TODO: inline an extra wrapping array after resolving https://github.com/aeternity/protocol/issues/505 accounts: array(mptree(Encoding.AccountAddress, Tag.Account)), calls: array(mptree(Encoding.Bytearray, Tag.ContractCall)), channels: array(mptree(Encoding.Channel, Tag.Channel)), @@ -426,17 +495,17 @@ export const txSchema = [{ oracles: array(mptree(Encoding.OracleAddress, Tag.Oracle)), }, { tag: shortUIntConst(Tag.StateTrees), - version: shortUIntConst(1, true), - contracts: entryAny, - calls: entryAny, - channels: entryAny, - ns: entryAny, - oracles: entryAny, - accounts: entryAny, + version: shortUIntConst(0, true), + contracts: wrapped(Tag.ContractsMtree) as unknown as MapContracts, + calls: wrapped(Tag.CallsMtree) as unknown as MapCalls, + channels: wrapped(Tag.ChannelsMtree) as unknown as MapChannels, + ns: wrapped(Tag.NameserviceMtree) as unknown as MapNames, + oracles: wrapped(Tag.OraclesMtree) as unknown as MapOracles, + accounts: wrapped(Tag.AccountsMtree) as unknown as MapAccounts, }, { tag: shortUIntConst(Tag.Mtree), version: shortUIntConst(1, true), - values: array(entry()) as unknown as EntryAnyArray, + values: entryMtreeValueArray, }, { tag: shortUIntConst(Tag.MtreeValue), version: shortUIntConst(1, true), @@ -445,27 +514,27 @@ export const txSchema = [{ }, { tag: shortUIntConst(Tag.ContractsMtree), version: shortUIntConst(1, true), - contracts: entryAny, + payload: mapContracts, }, { tag: shortUIntConst(Tag.CallsMtree), version: shortUIntConst(1, true), - calls: entryAny, + payload: mapCalls, }, { tag: shortUIntConst(Tag.ChannelsMtree), version: shortUIntConst(1, true), - channels: entryAny, + payload: mapChannels, }, { tag: shortUIntConst(Tag.NameserviceMtree), version: shortUIntConst(1, true), - mtree: entryAny, + payload: mapNames, }, { tag: shortUIntConst(Tag.OraclesMtree), version: shortUIntConst(1, true), - otree: entryAny, + payload: mapOracles, }, { tag: shortUIntConst(Tag.AccountsMtree), version: shortUIntConst(1, true), - accounts: entryAny, + payload: mapAccounts, }, { tag: shortUIntConst(Tag.GaAttachTx), version: shortUIntConst(1, true), diff --git a/test/unit/tx.ts b/test/unit/tx.ts index f73734ac3a..2048edd7e1 100644 --- a/test/unit/tx.ts +++ b/test/unit/tx.ts @@ -154,26 +154,26 @@ describe('Tx', () => { const address = 'ak_i9svRuk9SJfAponRnCYVnVWN9HVLdBEd8ZdGREJMaUiTn4S4D'; const account = { - tag: 10, version: 1, nonce: 0, balance: '99999999999999998997', + tag: Tag.Account, version: 1, nonce: 0, balance: '99999999999999998997', }; expect(unpackedPoi.accounts[0].get(address)).to.eql(account); const addressContract = 'ct_ECdrEy2NJKq3qK3xraPtcDP7vfdi56SQXYAH3bVVSTmpqpYyW'; const accountContract = { - tag: 10, version: 1, nonce: 0, balance: '1000', + tag: Tag.Account, version: 1, nonce: 0, balance: '1000', }; expect(unpackedPoi.accounts[0].get(addressContract as Encoded.AccountAddress)) .to.eql(accountContract); expect(unpackedPoi.accounts[0].toObject()).to.eql({ ak_BvMjyAXbpHkjzVfG53N6FxF1LwTX2EYwFLfNbk8mcXjp8CXBC: { - tag: 10, version: 1, nonce: 0, balance: '100000000000000000003', + tag: Tag.Account, version: 1, nonce: 0, balance: '100000000000000000003', }, [addressContract.replace('ct_', 'ak_')]: accountContract, [address]: account, }); const contract = { - tag: 40, + tag: Tag.Contract, version: 1, owner: 'ak_i9svRuk9SJfAponRnCYVnVWN9HVLdBEd8ZdGREJMaUiTn4S4D', ctVersion: { vmVersion: VmVersion.Fate, abiVersion: AbiVersion.Fate }, @@ -188,6 +188,119 @@ describe('Tx', () => { expect(buildTx(unpackedPoi, { prefix: Encoding.Poi })).to.equal(poi); }); + + it('unpacks state channel calls record', () => { + const tx = 'cs_+QFBggJuAbkBOvkBNz8B+QEyuJf4lUABuEBRt/rxUTPwSp8BBMQWR70Ag6kTiNXTDmO2LWStsbEXhED9reWbZWfYmFIwTrKG6Khrbb7SvfC4T4ll2BtXsX/luE/4TSkCoQFZYYdOS6IFcvCPFPCBUOkebcCW0ZehLaMRA+K9RcniKwICoQVRt/rxUTPwSp8BBMQWR70Ag6kTiNXTDmO2LWStsbEXhAA9PwDAuJf4lUABuEBRt/rxUTPwSp8BBMQWR70Ag6kTiNXTDmO2LWStsbEXhADlKPCJlyMdCDQrNsajcRCzQk6M8LSJvbdnJ7Lc4aFjuE/4TSkCoQFZYYdOS6IFcvCPFPCBUOkebcCW0ZehLaMRA+K9RcniKwMDoQVRt/rxUTPwSp8BBMQWR70Ag6kTiNXTDmO2LWStsbEXhAEOVADAenqUfg=='; + const params = { + tag: Tag.CallsMtree, + version: 1, + payload: { + 'ba_Ubf68VEz8EqfAQTEFke9AIOpE4jV0w5jti1krbGxF4RA/a3lm2Vn2JhSME6yhuioa22+0r3wuE+JZdgbV7F/5c9Ms1g=': { + callerId: 'ak_gN7nP72rm7D1kuSYWRtL9Sf4pFRoTPKM8wa9JHyneazW8zHm4', + callerNonce: 2, + contractId: 'ct_czPqotjcUujiXu5DaTeJMbv2WJpqwuhsQFn6edrGVRaoHLifk', + gasPrice: '0', + gasUsed: 61, + height: 2, + log: [], + returnType: 0, + returnValue: 'cb_P4fvHVw=', + tag: Tag.ContractCall, + version: 2, + }, + 'ba_Ubf68VEz8EqfAQTEFke9AIOpE4jV0w5jti1krbGxF4QA5SjwiZcjHQg0KzbGo3EQs0JOjPC0ib23Zyey3OGhY+BjbKc=': { + callerId: 'ak_gN7nP72rm7D1kuSYWRtL9Sf4pFRoTPKM8wa9JHyneazW8zHm4', + callerNonce: 3, + contractId: 'ct_czPqotjcUujiXu5DaTeJMbv2WJpqwuhsQFn6edrGVRaoHLifk', + gasPrice: '1', + gasUsed: 14, + height: 3, + log: [], + returnType: 0, + returnValue: 'cb_VNLOFXc=', + tag: Tag.ContractCall, + version: 2, + }, + }, + } as const; + expect(unpackTx(tx, Tag.CallsMtree)).to.be.eql(params); + expect(buildTx(params, { prefix: Encoding.CallStateTree })).to.be.equal(tx); + }); + + it('unpacks state channel signed tx', () => { + const { signatures, encodedTx } = unpackTx( + 'tx_+NILAfiEuEBCv6dwkalvFkuHyYNcRpgZVYlSMmyOO9ukCrBBYYy2zLdgaSs/ug3e01ep2jiy6z9ABOkC83QNpCjdi0eAahUBuEC1RFFr7z4401oJRENrqGRlRsOwTp/GU70W5zeiTP0TZ8rtfzhGH1ZjIsq7u+o6duevI+eyrBtXr3yeqbViEB4KuEj4RjkCoQYzv70uksCUiH6SlOGVAYhx0LkLFmtDUXsRejThITz2MwOgGK/uHihyT8uUtXTAcncw9QFkW0QghCzEWDfwXWbHR14jXFqu', + Tag.SignedTx, + ); + expect(signatures).to.have.lengthOf(2); + if (encodedTx.tag !== Tag.ChannelOffChainTx) throw new Error('Unexpected nested transaction'); + expect(encodedTx.channelId).to.be.satisfy((s: string) => s.startsWith('ch_')); + expect(encodedTx.round).to.be.equal(3); + expect(encodedTx.stateHash).to.be.satisfy((s: string) => s.startsWith('st_')); + }); + + it('unpacks state trees tx', () => { + const tx = 'ss_+QKqPgC5ATb5ATOCAm0BuQEs+QEpPwH5ASSo50ABo/v6skWp8mq1jwV/+iKDkHayfTtp7ytW6d/nZ2QVQ4zhEAABP6rpQAGj+/qyRanyarWPBX/6IoOQdrJ9O2nvK1bp3+dnZBVDjOEQAACCLwCm5UABofv6skWp8mq1jwV/+iKDkHayfTtp7ytW6d/nZ2QVQ4zhEAC4p/ilQAGg+/qyRanyarWPBX/6IoOQdrJ9O2nvK1bp3+dnZBVDjOG4gPh+KAGhAaJtvnDfkCeML/RLVY1N/6eQNHADrvu4hZoCmMAbCi5SgwUAA7hO+ExGA6Dh3797nquCHCSm088avgOiqgjjarRQviEYAXq+YlegWcCgjf6AeCCSADcBBwcBAQCOLwERgHggkhlnZXRBcmeCLwCFNy4wLjEAgAHAggPouKf4pYICbgG4n/idPwH4mbiX+JVAAbhA+/qyRanyarWPBX/6IoOQdrJ9O2nvK1bp3+dnZBVDjOEHycljzso7F0NwAzM/Oj84MV1Lo7ia3VslsuGHHCI+wrhP+E0pAqEBom2+cN+QJ4wv9EtVjU3/p5A0cAOu+7iFmgKYwBsKLlICAqEF+/qyRanyarWPBX/6IoOQdrJ9O2nvK1bp3+dnZBVDjOEAPT8AwIrJggJvAYTDPwHAismCAnABhMM/AcCKyYICcQGEwz8BwLij+KGCAnIBuJv4mT8B+JWs60ABoPv6skWp8mq1jwV/+iKDkHayfTtp7ytW6d/nZ2QVQ4zhh8YKAQCCA+iz8kABoKJtvnDfkCeML/RLVY1N/6eQNHADrvu4hZoCmMAbCi5Sjs0KAQCJBWvHXi1jD/wVs/JAAaBuFXSR/8BDssKtH01lCLrV/Jd1ImY9KNci1mQqB0RIJY7NCgEAiQVrx14tYxAAA0X2l9c='; + const params = { + accounts: { + ak_2uyUQn1dyzrMxjzhSQgZ2rV1dk2D5BCYpquzzBn6hxoSAo7y1d: { + balance: '1000', + nonce: 0, + tag: Tag.Account, + version: 1, + }, + ak_2EY2KjfhXkpLq2u13YuvDBahi8Yxq5ErNocCeCUAvwHmJjS2aF: { + balance: '99999999999999998997', + nonce: 0, + tag: Tag.Account, + version: 1, + }, + ak_qUwhrGsBqhxh2Ace9KBsrDtJpmFeWhuhLZg61L4cQ4Lknhvud: { + balance: '100000000000000000003', + nonce: 0, + tag: Tag.Account, + version: 1, + }, + }, + calls: { + 'ba_+/qyRanyarWPBX/6IoOQdrJ9O2nvK1bp3+dnZBVDjOEHycljzso7F0NwAzM/Oj84MV1Lo7ia3VslsuGHHCI+wsMqg1w=': { + callerId: 'ak_2EY2KjfhXkpLq2u13YuvDBahi8Yxq5ErNocCeCUAvwHmJjS2aF', + callerNonce: 2, + contractId: 'ct_2uyUQn1dyzrMxjzhSQgZ2rV1dk2D5BCYpquzzBn6hxoSAo7y1d', + gasPrice: '0', + gasUsed: 61, + height: 2, + log: [], + returnType: 0, + returnValue: 'cb_P4fvHVw=', + tag: Tag.ContractCall, + version: 2, + }, + }, + channels: {}, + contracts: { + ct_2uyUQn1dyzrMxjzhSQgZ2rV1dk2D5BCYpquzzBn6hxoSAo7y1d: { + active: true, + code: 'cb_+ExGA6Dh3797nquCHCSm088avgOiqgjjarRQviEYAXq+YlegWcCgjf6AeCCSADcBBwcBAQCOLwERgHggkhlnZXRBcmeCLwCFNy4wLjEALb9eTg==', + ctVersion: { + abiVersion: 3, + vmVersion: 5, + }, + deposit: '1000', + log: 'cb_Xfbg4g==', + owner: 'ak_2EY2KjfhXkpLq2u13YuvDBahi8Yxq5ErNocCeCUAvwHmJjS2aF', + referers: [], + tag: Tag.Contract, + version: 1, + }, + }, + ns: {}, + oracles: {}, + tag: Tag.StateTrees, + version: 0, + } as const; + expect(unpackTx(tx, Tag.StateTrees)).to.be.eql(params); + }); }); const address = 'ak_i9svRuk9SJfAponRnCYVnVWN9HVLdBEd8ZdGREJMaUiTn4S4D';