From 04956df28da2a81595caaac0cf989840c3735f6d Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Mon, 12 May 2025 15:19:59 -0300 Subject: [PATCH 01/23] feat: method argument value class --- src/nano_contracts/deserializer.ts | 167 ++++++++++++++++++----------- src/nano_contracts/methodArg.ts | 94 ++++++++++++++++ src/nano_contracts/parser.ts | 16 ++- src/nano_contracts/types.ts | 54 +++++++++- 4 files changed, 262 insertions(+), 69 deletions(-) create mode 100644 src/nano_contracts/methodArg.ts diff --git a/src/nano_contracts/deserializer.ts b/src/nano_contracts/deserializer.ts index 95acac3d7..e415ea3e2 100644 --- a/src/nano_contracts/deserializer.ts +++ b/src/nano_contracts/deserializer.ts @@ -9,16 +9,11 @@ import { bufferToHex, unpackToInt } from '../utils/buffer'; import helpersUtils from '../utils/helpers'; import leb128Util from '../utils/leb128'; import Network from '../models/network'; -import { NanoContractArgumentType } from './types'; +import { NanoContractArgumentType, BufferROExtract, NanoContractSignedData, NanoContractArgumentSingleType, NanoContractRawSignedData } from './types'; import { OutputValueType } from '../types'; import { NC_ARGS_MAX_BYTES_LENGTH } from '../constants'; import { getContainerInternalType, getContainerType } from './utils'; -interface DeserializeResult { - value: T; - bytesRead: number; -} - class Deserializer { network: Network; @@ -40,7 +35,7 @@ class Deserializer { deserializeFromType( buf: Buffer, type: string - ): DeserializeResult { + ): BufferROExtract { const isContainerType = getContainerType(type) !== null; if (isContainerType) { return this.deserializeContainerType(buf, type); @@ -75,14 +70,17 @@ class Deserializer { deserializeContainerType( buf: Buffer, type: string - ): DeserializeResult { + ): BufferROExtract { const [containerType, internalType] = getContainerInternalType(type); switch (containerType) { case 'Optional': return this.toOptional(buf, internalType); case 'SignedData': - return this.toSigned(buf, type); // XXX: change to internalType? + return this.toSignedData(buf, internalType); + case 'RawSignedData': + case 'Tuple': + throw new Error('Not implemented yet'); default: throw new Error('Invalid type.'); } @@ -90,6 +88,7 @@ class Deserializer { /* eslint-disable class-methods-use-this -- XXX: Methods that don't use `this` should be made static */ + /** * Deserialize string value * @@ -98,7 +97,7 @@ class Deserializer { * @memberof Deserializer * @inner */ - toString(buf: Buffer): DeserializeResult { + toString(buf: Buffer): BufferROExtract { // INFO: maxBytes is set to 3 becuase the max allowed length in bytes for a string is // NC_ARGS_MAX_BYTES_LENGTH which is encoded as 3 bytes in leb128 unsigned. // If we read a fourth byte we are definetely reading a higher number than allowed. @@ -129,7 +128,7 @@ class Deserializer { * @memberof Deserializer * @inner */ - toBytes(buf: Buffer): DeserializeResult { + toBytes(buf: Buffer): BufferROExtract { // INFO: maxBytes is set to 3 becuase the max allowed length in bytes for a string is // NC_ARGS_MAX_BYTES_LENGTH which is encoded as 3 bytes in leb128 unsigned. // If we read a fourth byte we are definetely reading a higher number than allowed. @@ -160,7 +159,7 @@ class Deserializer { * @memberof Deserializer * @inner */ - toInt(buf: Buffer): DeserializeResult { + toInt(buf: Buffer): BufferROExtract { return { value: unpackToInt(4, true, buf)[0], bytesRead: 4, @@ -175,7 +174,7 @@ class Deserializer { * @memberof Deserializer * @inner */ - toAmount(buf: Buffer): DeserializeResult { + toAmount(buf: Buffer): BufferROExtract { // Nano `Amount` currently only supports up to 4 bytes, so we simply use the `number` value converted to `BigInt`. // If we change Nano to support up to 8 bytes, we must update this. const { value, bytesRead } = this.toInt(buf); @@ -193,7 +192,7 @@ class Deserializer { * @memberof Deserializer * @inner */ - toBool(buf: Buffer): DeserializeResult { + toBool(buf: Buffer): BufferROExtract { if (buf[0]) { return { value: true, @@ -213,13 +212,49 @@ class Deserializer { * * @memberof Deserializer */ - toVarInt(buf: Buffer): DeserializeResult { + toVarInt(buf: Buffer): BufferROExtract { const { value, bytesRead } = leb128Util.decodeSigned(buf); return { value, bytesRead }; } /* eslint-enable class-methods-use-this */ + /** + * Deserialize a value decoded in bytes to a base58 string + * + * @param {Buffer} buf Value to deserialize + * + * @memberof Deserializer + * @inner + */ + toAddress(buf: Buffer): BufferROExtract { + const lenReadResult = leb128Util.decodeUnsigned(buf, 1); + if (lenReadResult.value !== 25n) { + // Address should be exactly 25 bytes long + throw new Error('Address should be 25 bytes long'); + } + // First we get the 20 bytes of the address without the version byte and checksum + const addressBytes = buf.subarray(2, 22); + const address = helpersUtils.encodeAddress(addressBytes, this.network); + address.validateAddress(); + const decoded = address.decode(); + if (decoded[0] !== buf[1]) { + throw new Error( + `Asked to deserialize an address with version byte ${buf[0]} but the network from the deserializer object has version byte ${decoded[0]}.` + ); + } + if (decoded.subarray(21, 25).toString('hex') !== buf.subarray(22, 26).toString('hex')) { + // Checksum value generated does not match value from fullnode + throw new Error( + `When parsing and Address(${address.base58}) we calculated checksum(${decoded.subarray(21, 25).toString('hex')}) but it does not match the checksum it came with ${buf.subarray(22, 26).toString('hex')}.` + ); + } + return { + value: address.base58, + bytesRead: 26, // 1 for length + 25 address bytes + }; + } + /** * Deserialize an optional value * @@ -233,7 +268,7 @@ class Deserializer { * @memberof Deserializer * @inner */ - toOptional(buf: Buffer, type: string): DeserializeResult { + toOptional(buf: Buffer, type: string): BufferROExtract { if (buf[0] === 0) { // It's an empty optional return { @@ -251,6 +286,37 @@ class Deserializer { }; } + toSignedData(signedData: Buffer, type: string): BufferROExtract { + // The SignedData is serialized as `ContractId+data+Signature` + + // Reading ContractId + const ncIdResult = this.deserializeFromType(signedData, 'ContractId'); + const ncId = (ncIdResult.value as Buffer); + const bytesReadFromContractId = ncIdResult.bytesRead; + + let buf = signedData.subarray(bytesReadFromContractId); + + // Reading argument + const parseResult = this.deserializeFromType(buf, type); + let parsed = parseResult.value; + const bytesReadFromValue = parseResult.bytesRead; + + // Reading signature as bytes + const { value: parsedSignature, bytesRead: bytesReadFromSignature } = this.deserializeFromType( + buf.subarray(bytesReadFromValue), + 'bytes' + ); + + return { + value: { + type, + value: [ncId, parsed as NanoContractArgumentSingleType], + signature: parsedSignature as Buffer, + }, + bytesRead: bytesReadFromContractId + bytesReadFromValue + bytesReadFromSignature, + }; + } + /** * Deserialize a signed value * @@ -263,33 +329,15 @@ class Deserializer { * @memberof Deserializer * @inner */ - toSigned(signedData: Buffer, type: string): DeserializeResult { - const [containerType, internalType] = getContainerInternalType(type); - if (containerType !== 'SignedData') { - throw new Error('Type is not SignedData'); - } - if (!internalType) { - throw new Error('Unable to extract type'); - } - // Should we check that the valueType is valid? - - // SignData[T] is serialized as Serialize(T)+Serialize(sign(T)) where sign() returns a byte str + toRawSignedData(signedData: Buffer, type: string): BufferROExtract { + // RawSignData[T] is serialized as Serialize(T)+Serialize(sign(T)) where sign() returns a byte str // Which means we can parse the T argument, then read the bytes after. // Reading argument - const parseResult = this.deserializeFromType(signedData, internalType); + const parseResult = this.deserializeFromType(signedData, type); let parsed = parseResult.value; const bytesReadFromValue = parseResult.bytesRead; - if (internalType === 'bytes') { - // If the value is bytes, we should transform into hex to return the string - parsed = bufferToHex(parsed as Buffer); - } - - if (internalType === 'bool') { - parsed = (parsed as boolean) ? 'true' : 'false'; - } - // Reading signature const { value: parsedSignature, bytesRead: bytesReadFromSignature } = this.deserializeFromType( signedData.subarray(bytesReadFromValue), @@ -297,45 +345,38 @@ class Deserializer { ); return { - value: `${bufferToHex(parsedSignature as Buffer)},${parsed},${internalType}`, + value: { + type, + value: parsed as NanoContractArgumentSingleType, + signature: parsedSignature as Buffer, + }, bytesRead: bytesReadFromValue + bytesReadFromSignature, }; } /** - * Deserialize a value decoded in bytes to a base58 string + * Deserialize string value * * @param {Buffer} buf Value to deserialize * * @memberof Deserializer * @inner */ - toAddress(buf: Buffer): DeserializeResult { - const lenReadResult = leb128Util.decodeUnsigned(buf, 1); - if (lenReadResult.value !== 25n) { - // Address should be exactly 25 bytes long - throw new Error('Address should be 25 bytes long'); - } - // First we get the 20 bytes of the address without the version byte and checksum - const addressBytes = buf.subarray(2, 22); - const address = helpersUtils.encodeAddress(addressBytes, this.network); - address.validateAddress(); - const decoded = address.decode(); - if (decoded[0] !== buf[1]) { - throw new Error( - `Asked to deserialize an address with version byte ${buf[0]} but the network from the deserializer object has version byte ${decoded[0]}.` - ); - } - if (decoded.subarray(21, 25).toString('hex') !== buf.subarray(22, 26).toString('hex')) { - // Checksum value generated does not match value from fullnode - throw new Error( - `When parsing and Address(${address.base58}) we calculated checksum(${decoded.subarray(21, 25).toString('hex')}) but it does not match the checksum it came with ${buf.subarray(22, 26).toString('hex')}.` - ); + toTuple(buf: Buffer, type: string): BufferROExtract> { + const typeArr = type.split(',').map(s => s.trim()); + const tupleValues: NanoContractArgumentType[] = [] + let bytesReadTotal = 0; + let tupleBuf = buf.subarray(); + for (const t of typeArr) { + const result = this.deserializeFromType(tupleBuf, t); + tupleValues.push(result.value); + bytesReadTotal += result.bytesRead; + tupleBuf = tupleBuf.subarray(result.bytesRead); } return { - value: address.base58, - bytesRead: 26, // 1 for length + 25 address bytes - }; + value: tupleValues, + bytesRead: bytesReadTotal, + } } } diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts new file mode 100644 index 000000000..3d5f4a646 --- /dev/null +++ b/src/nano_contracts/methodArg.ts @@ -0,0 +1,94 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + NanoContractArgumentSingleType, + NanoContractArgumentType, + NanoContractParsedArgument, + NanoContractRawSignedData, + NanoContractSignedData, + BufferROExtract, +} from './types'; +import Serializer from './serializer'; +import Deserializer from './deserializer'; +import { getContainerInternalType } from './utils'; + +export class NanoContractMethodArgument { + name: string; + type: string; + value: NanoContractArgumentType; + _serialized: Buffer; + + constructor(name: string, type: string, value: NanoContractArgumentType) { + this.name = name; + this.type = type; + this.value = value; + this._serialized = Buffer.alloc(0); + } + + serialize(serializer: Serializer): Buffer { + if (this._serialized.length === 0) { + this._serialized = serializer.serializeFromType(this.value, this.type) + } + + return this._serialized + } + + static fromSerialized(name: string, type: string, buf: Buffer, deserializer: Deserializer): BufferROExtract { + const parseResult = deserializer.deserializeFromType(buf, type); + return { + value: new NanoContractMethodArgument(name, type, parseResult.value), + bytesRead: parseResult.bytesRead, + } + } + + toHumanReadable(): NanoContractParsedArgument { + function prepSingleValue(type: string, value: NanoContractArgumentSingleType) { + switch(type) { + case 'bytes': + return (value as Buffer).toString('hex'); + case 'bool': + return (value as boolean) ? 'true' : 'false'; + default: + return value; + } + } + + if (this.type.startsWith('SignedData')) { + const data = this.value as NanoContractSignedData; + return { + name: this.name, + type: this.type, + parsed: [ + data.signature.toString('hex'), + data.value[0].toString('hex'), + prepSingleValue(data.type, data.value[1]), + this.type, + ].join(','), + } + } + + if (this.type.startsWith('RawSignedData')) { + const data = this.value as NanoContractRawSignedData; + return { + name: this.name, + type: this.type, + parsed: [ + data.signature.toString('hex'), + prepSingleValue(data.type, data.value), + this.type, + ].join(','), + } + } + + return { + name: this.name, + type: this.type, + parsed: this.value, + } + } +} diff --git a/src/nano_contracts/parser.ts b/src/nano_contracts/parser.ts index 9cd58b671..516641494 100644 --- a/src/nano_contracts/parser.ts +++ b/src/nano_contracts/parser.ts @@ -14,6 +14,7 @@ import { getAddressFromPubkey } from '../utils/address'; import { NanoContractTransactionParseError } from '../errors'; import { MethodArgInfo, NanoContractArgumentType, NanoContractParsedArgument } from './types'; import leb128 from '../utils/leb128'; +import { NanoContractMethodArgument } from './methodArg'; class NanoContractTransactionParser { blueprintId: string; @@ -28,7 +29,7 @@ class NanoContractTransactionParser { args: string | null; - parsedArgs: NanoContractParsedArgument[] | null; + parsedArgs: NanoContractMethodArgument[] | null; constructor( blueprintId: string, @@ -63,7 +64,7 @@ class NanoContractTransactionParser { * @inner */ async parseArguments() { - const parsedArgs: NanoContractParsedArgument[] = []; + const parsedArgs: NanoContractMethodArgument[] = []; if (!this.args) { return; } @@ -95,17 +96,22 @@ class NanoContractTransactionParser { } for (const arg of methodArgs) { - let parsed: NanoContractArgumentType; + let parsed: NanoContractMethodArgument; let size: number; try { - const parseResult = deserializer.deserializeFromType(argsBuffer, arg.type); + const parseResult = NanoContractMethodArgument.fromSerialized( + arg.name, + arg.type, + argsBuffer, + deserializer, + ); parsed = parseResult.value; size = parseResult.bytesRead; } catch (err: unknown) { console.error(err); throw new NanoContractTransactionParseError(`Failed to deserialize argument ${arg.type}.`); } - parsedArgs.push({ ...arg, parsed }); + parsedArgs.push(parsed); argsBuffer = argsBuffer.subarray(size); } // XXX: argsBuffer should be empty since we parsed all arguments diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 0d057fe7a..01fda5461 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -6,6 +6,10 @@ */ import { IHistoryTx, OutputValueType } from '../types'; +/** + * There are the types that can be received via api + * when querying for a nano contract value. + */ export type NanoContractArgumentApiInputType = | string | number @@ -13,8 +17,51 @@ export type NanoContractArgumentApiInputType = | OutputValueType | boolean | null; -export type NanoContractArgumentType = NanoContractArgumentApiInputType | Buffer; +/** + * These are the possible `Single` types after parsing + * We include Buffer since some types are decoded as Buffer (e.g. bytes, TokenUid, ContractId) + */ +export type NanoContractArgumentSingleType = NanoContractArgumentApiInputType | Buffer; + +/** + * A `SignedData` value is the tuple `[ContractId, Value]` + * which is parsed as `[Buffer, NanoContractArgumentSingleType]` + */ +export type NanoContractSignedDataInnerType = [Buffer, NanoContractArgumentSingleType]; + +/** + * NanoContract SignedData method argument type + */ +export type NanoContractSignedData = { + type: string; + value: NanoContractSignedDataInnerType; + signature: Buffer; +} + +/** + * NanoContract RawSignedData method argument type + */ +export type NanoContractRawSignedData = { + type: string; + value: NanoContractArgumentSingleType; + signature: Buffer; +} + +/** + * Intermediate type for all possible Nano contract argument type + * that do not include tuple/arrays/repetition + */ +type _NanoContractArgumentType1 = NanoContractArgumentSingleType | NanoContractSignedData | NanoContractRawSignedData; + +/** + * Nano Contract method argument type as a native TS type + */ +export type NanoContractArgumentType = _NanoContractArgumentType1 | _NanoContractArgumentType1[]; + +/** + * Container type names + */ export type NanoContractArgumentContainerType = 'Optional' | 'SignedData' | 'RawSignedData' | 'Tuple'; export enum NanoContractActionType { @@ -137,3 +184,8 @@ export interface NanoContractStateAPIParameters { block_hash?: string; block_height?: number; } + +export type BufferROExtract = { + value: T; + bytesRead: number; +} From 16b4634b9c0fdc4219e15b9202ff608cb1f724cb Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Mon, 12 May 2025 16:14:52 -0300 Subject: [PATCH 02/23] chore: run tests --- .github/workflows/integration-test.yml | 2 ++ .github/workflows/lint.yml | 2 ++ .github/workflows/unit-test.yml | 2 ++ src/nano_contracts/methodArg.ts | 1 - 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 360c89222..373d37a45 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -5,6 +5,7 @@ on: - master - release - release-candidate + - feat/nc-args-serialization tags: - v* pull_request: @@ -12,6 +13,7 @@ on: - release - release-candidate - master + - feat/nc-args-serialization env: TEST_WALLET_START_TIMEOUT: '180000' # 3 minutes diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 10540c656..f5802b0bf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,6 +5,7 @@ on: - master - release - release-candidate + - feat/nc-args-serialization tags: - v* pull_request: @@ -12,6 +13,7 @@ on: - release - release-candidate - master + - feat/nc-args-serialization jobs: linter: runs-on: 'ubuntu-latest' diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 49620a431..627ad75c7 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -5,6 +5,7 @@ on: - master - release - release-candidate + - feat/nc-args-serialization tags: - v* pull_request: @@ -12,6 +13,7 @@ on: - release - release-candidate - master + - feat/nc-args-serialization jobs: test: runs-on: 'ubuntu-latest' diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index 3d5f4a646..6a89857e5 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -15,7 +15,6 @@ import { } from './types'; import Serializer from './serializer'; import Deserializer from './deserializer'; -import { getContainerInternalType } from './utils'; export class NanoContractMethodArgument { name: string; From d5606905cc230ffd6d95ae3d96db22fad3c0d8e8 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 00:40:28 -0300 Subject: [PATCH 03/23] feat: SignedData serialization --- __tests__/nano_contracts/deserializer.test.ts | 101 +++++++++++--- __tests__/nano_contracts/methodArg.test.ts | 122 ++++++++++++++++ __tests__/nano_contracts/serializer.test.ts | 71 ++++++++-- src/nano_contracts/builder.ts | 7 +- src/nano_contracts/deserializer.ts | 28 ++-- src/nano_contracts/methodArg.ts | 123 ++++++++++++++-- src/nano_contracts/parser.ts | 2 +- src/nano_contracts/serializer.ts | 131 +++++++++--------- src/nano_contracts/types.ts | 56 +++++--- src/nano_contracts/utils.ts | 10 +- 10 files changed, 498 insertions(+), 153 deletions(-) create mode 100644 __tests__/nano_contracts/methodArg.test.ts diff --git a/__tests__/nano_contracts/deserializer.test.ts b/__tests__/nano_contracts/deserializer.test.ts index 25bc15e92..3ca019afa 100644 --- a/__tests__/nano_contracts/deserializer.test.ts +++ b/__tests__/nano_contracts/deserializer.test.ts @@ -9,6 +9,8 @@ import Serializer from '../../src/nano_contracts/serializer'; import Deserializer from '../../src/nano_contracts/deserializer'; import Address from '../../src/models/address'; import Network from '../../src/models/network'; +import leb128 from '../../src/utils/leb128'; +import { NanoContractSignedData } from '../../src/nano_contracts/types'; test('Bool', () => { const serializer = new Serializer(new Network('testnet')); @@ -184,59 +186,114 @@ test('SignedData', () => { const serializer = new Serializer(new Network('testnet')); const deserializer = new Deserializer(new Network('testnet')); - const valueInt = '74657374,300,int'; + const valueInt: NanoContractSignedData = { + type: 'int', + value: [Buffer.from('6e634944', 'hex'), 300], + signature: Buffer.from('74657374', 'hex'), + }; const serializedInt = serializer.serializeFromType(valueInt, 'SignedData[int]'); const { value: deserializedInt } = deserializer.deserializeFromType( serializedInt, 'SignedData[int]' ); - expect(deserializedInt).toBe(valueInt); - - const valueStr = '74657374,test,str'; + expect((deserializedInt as NanoContractSignedData).type).toEqual(valueInt.type); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedInt as NanoContractSignedData).signature).toMatchBuffer(valueInt.signature); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedInt as NanoContractSignedData).value[0]).toMatchBuffer(valueInt.value[0]); + expect((deserializedInt as NanoContractSignedData).value[1]).toEqual(valueInt.value[1]); + + const valueStr: NanoContractSignedData = { + type: 'str', + value: [Buffer.from('6e634944', 'hex'), 'test'], + signature: Buffer.from('74657374', 'hex'), + }; const serializedStr = serializer.serializeFromType(valueStr, 'SignedData[str]'); const { value: deserializedStr } = deserializer.deserializeFromType( serializedStr, 'SignedData[str]' ); - expect(deserializedStr).toBe(valueStr); - - const valueBytes = '74657374,74657374,bytes'; + expect((deserializedStr as NanoContractSignedData).type).toEqual(valueStr.type); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedStr as NanoContractSignedData).signature).toMatchBuffer(valueStr.signature); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedStr as NanoContractSignedData).value[0]).toMatchBuffer(valueStr.value[0]); + expect((deserializedStr as NanoContractSignedData).value[1]).toEqual(valueStr.value[1]); + + const valueBytes: NanoContractSignedData = { + type: 'bytes', + value: [Buffer.from('6e634944', 'hex'), Buffer.from('74657374', 'hex')], + signature: Buffer.from('74657374', 'hex'), + }; const serializedBytes = serializer.serializeFromType(valueBytes, 'SignedData[bytes]'); const { value: deserializedBytes } = deserializer.deserializeFromType( serializedBytes, 'SignedData[bytes]' ); - expect(deserializedBytes).toBe(valueBytes); + expect((deserializedBytes as NanoContractSignedData).type).toEqual(valueBytes.type); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedBytes as NanoContractSignedData).signature).toMatchBuffer(valueBytes.signature); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedBytes as NanoContractSignedData).value[0]).toMatchBuffer(valueBytes.value[0]); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedBytes as NanoContractSignedData).value[1]).toMatchBuffer(valueBytes.value[1]); - const valueBoolFalse = '74657374,false,bool'; + const valueBoolFalse: NanoContractSignedData = { + type: 'bool', + value: [Buffer.from('6e634944', 'hex'), false], + signature: Buffer.from('74657374', 'hex'), + }; const serializedBoolFalse = serializer.serializeFromType(valueBoolFalse, 'SignedData[bool]'); const { value: deserializedBoolFalse } = deserializer.deserializeFromType( serializedBoolFalse, 'SignedData[bool]' ); - expect(deserializedBoolFalse).toBe(valueBoolFalse); - - const valueBoolTrue = '74657374,true,bool'; + expect((deserializedBoolFalse as NanoContractSignedData).type).toEqual(valueBoolFalse.type); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedBoolFalse as NanoContractSignedData).signature).toMatchBuffer(valueBoolFalse.signature); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedBoolFalse as NanoContractSignedData).value[0]).toMatchBuffer(valueBoolFalse.value[0]); + expect((deserializedBoolFalse as NanoContractSignedData).value[1]).toEqual(valueBoolFalse.value[1]); + + const valueBoolTrue: NanoContractSignedData = { + type: 'bool', + value: [Buffer.from('6e634944', 'hex'), true], + signature: Buffer.from('74657374', 'hex'), + }; const serializedBoolTrue = serializer.serializeFromType(valueBoolTrue, 'SignedData[bool]'); const { value: deserializedBoolTrue } = deserializer.deserializeFromType( serializedBoolTrue, 'SignedData[bool]' ); - expect(deserializedBoolTrue).toBe(valueBoolTrue); - - const valueVarInt = '74657374,300,VarInt'; + expect((deserializedBoolTrue as NanoContractSignedData).type).toEqual(valueBoolTrue.type); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedBoolTrue as NanoContractSignedData).signature).toMatchBuffer(valueBoolTrue.signature); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedBoolTrue as NanoContractSignedData).value[0]).toMatchBuffer(valueBoolTrue.value[0]); + expect((deserializedBoolTrue as NanoContractSignedData).value[1]).toEqual(valueBoolTrue.value[1]); + + const valueVarInt: NanoContractSignedData = { + type: 'VarInt', + value: [Buffer.from('6e634944', 'hex'), 300n], + signature: Buffer.from('74657374', 'hex'), + }; const serializedVarInt = serializer.serializeFromType(valueVarInt, 'SignedData[VarInt]'); const { value: deserializedVarInt } = deserializer.deserializeFromType( serializedVarInt, 'SignedData[VarInt]' ); - expect(deserializedVarInt).toBe(valueVarInt); + expect((deserializedVarInt as NanoContractSignedData).type).toEqual(valueVarInt.type); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedVarInt as NanoContractSignedData).signature).toMatchBuffer(valueVarInt.signature); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((deserializedVarInt as NanoContractSignedData).value[0]).toMatchBuffer(valueVarInt.value[0]); + expect((deserializedVarInt as NanoContractSignedData).value[1]).toEqual(valueVarInt.value[1]); }); test('Address', () => { @@ -246,13 +303,21 @@ test('Address', () => { const address = 'WfthPUEecMNRs6eZ2m2EQBpVH6tbqQxYuU'; const addressBuffer = new Address(address).decode(); - const { value: deserialized } = deserializer.deserializeFromType(addressBuffer, 'Address'); + const { value: deserialized } = deserializer.deserializeFromType( + Buffer.concat([leb128.encodeUnsigned(addressBuffer.length), addressBuffer]), + 'Address'); expect(deserialized).toBe(address); const wrongNetworkAddress = 'HDeadDeadDeadDeadDeadDeadDeagTPgmn'; const wrongNetworkAddressBuffer = new Address(wrongNetworkAddress).decode(); - expect(() => deserializer.deserializeFromType(wrongNetworkAddressBuffer, 'Address')).toThrow(); + expect(() => deserializer.deserializeFromType( + Buffer.concat([ + leb128.encodeUnsigned(wrongNetworkAddressBuffer.length), + wrongNetworkAddressBuffer, + ]), + 'Address', + )).toThrow(); }); test('VarInt', () => { diff --git a/__tests__/nano_contracts/methodArg.test.ts b/__tests__/nano_contracts/methodArg.test.ts new file mode 100644 index 000000000..cba0f0b98 --- /dev/null +++ b/__tests__/nano_contracts/methodArg.test.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { NanoContractMethodArgument } from "../../src/nano_contracts/methodArg"; +import { NanoContractSignedData } from "../../src/nano_contracts/types"; + +describe('fromApiInput', () => { + it('should read SignedData[int]', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[int]', '74657374,6e634944,300,int') + expect(arg).toMatchObject({ + name: 'a-test', + type: 'SignedData[int]', + value: { + type: 'int', + signature: expect.anything(), + value: expect.anything(), + } + }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[1]).toEqual(300); + }); + + it('should read SignedData[VarInt]', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[VarInt]', '74657374,6e634944,300,VarInt') + expect(arg).toMatchObject({ + name: 'a-test', + type: 'SignedData[VarInt]', + value: { + type: 'VarInt', + signature: expect.anything(), + value: expect.anything(), + } + }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[1]).toEqual(300n); + }); + + it('should read SignedData[str]', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[str]', '74657374,6e634944,test,str') + expect(arg).toMatchObject({ + name: 'a-test', + type: 'SignedData[str]', + value: { + type: 'str', + signature: expect.anything(), + value: expect.anything(), + } + }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[1]).toEqual('test'); + }); + + it('should read SignedData[bytes]', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[bytes]', '74657374,6e634944,74657374,bytes') + expect(arg).toMatchObject({ + name: 'a-test', + type: 'SignedData[bytes]', + value: { + type: 'bytes', + signature: expect.anything(), + value: expect.anything(), + } + }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).value[1]).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + }); + + + it('should read true SignedData[bool]', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[bool]', '74657374,6e634944,true,bool') + expect(arg).toMatchObject({ + name: 'a-test', + type: 'SignedData[bool]', + value: { + type: 'bool', + signature: expect.anything(), + value: expect.anything(), + } + }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[1]).toEqual(true); + }); + + + it('should read false SignedData[bool]', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[bool]', '74657374,6e634944,false,bool') + expect(arg).toMatchObject({ + name: 'a-test', + type: 'SignedData[bool]', + value: { + type: 'bool', + signature: expect.anything(), + value: expect.anything(), + } + }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[1]).toEqual(false); + }); +}); diff --git a/__tests__/nano_contracts/serializer.test.ts b/__tests__/nano_contracts/serializer.test.ts index fc4f8ec36..a05444e82 100644 --- a/__tests__/nano_contracts/serializer.test.ts +++ b/__tests__/nano_contracts/serializer.test.ts @@ -87,36 +87,79 @@ test('Optional', () => { ); }); -test('Signed', () => { +test('SignedData', () => { const serializer = new Serializer(new Network('testnet')); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect(serializer.fromSigned('74657374,300,int')).toMatchBuffer( - Buffer.from([0x00, 0x00, 0x01, 0x2c, 0x04, 0x74, 0x65, 0x73, 0x74]) + expect(serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'int', + value: [Buffer.from('6e634944', 'hex'), 300], + }, + 'int', + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + )).toMatchBuffer( + // 4 + ncId + value + 4 + test + Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x00, 0x00, 0x01, 0x2c, 0x04, 0x74, 0x65, 0x73, 0x74]) ); + expect(serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'VarInt', + value: [Buffer.from('6e634944', 'hex'), 300n], + }, + 'VarInt', // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect(serializer.fromSigned('74657374,300,VarInt')).toMatchBuffer( - Buffer.from([0xac, 0x02, 0x04, 0x74, 0x65, 0x73, 0x74]) + )).toMatchBuffer( + Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0xac, 0x02, 0x04, 0x74, 0x65, 0x73, 0x74]) ); + expect(serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'str', + value: [Buffer.from('6e634944', 'hex'), 'test'], + }, + 'str', // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect(serializer.fromSigned('74657374,test,str')).toMatchBuffer( - Buffer.from([0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74]) + )).toMatchBuffer( + Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74]) ); + expect(serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'bytes', + value: [Buffer.from('6e634944', 'hex'), Buffer.from('74657374', 'hex')], + }, + 'bytes', // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect(serializer.fromSigned('74657374,74657374,bytes')).toMatchBuffer( - Buffer.from([0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74]) + )).toMatchBuffer( + Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74]) ); + expect(serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'bool', + value: [Buffer.from('6e634944', 'hex'), false], + }, + 'bool', // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect(serializer.fromSigned('74657374,false,bool')).toMatchBuffer( - Buffer.from([0x00, 0x04, 0x74, 0x65, 0x73, 0x74]) + )).toMatchBuffer( + Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74]) ); + expect(serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'bool', + value: [Buffer.from('6e634944', 'hex'), true], + }, + 'bool', // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect(serializer.fromSigned('74657374,true,bool')).toMatchBuffer( - Buffer.from([0x01, 0x04, 0x74, 0x65, 0x73, 0x74]) + )).toMatchBuffer( + Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74]) ); }); diff --git a/src/nano_contracts/builder.ts b/src/nano_contracts/builder.ts index c2ea5766a..ea59e4f1c 100644 --- a/src/nano_contracts/builder.ts +++ b/src/nano_contracts/builder.ts @@ -22,13 +22,13 @@ import { NanoContractAction, MethodArgInfo, NanoContractArgumentApiInputType, - NanoContractArgumentType, } from './types'; import { ITokenData } from '../types'; import ncApi from '../api/nano'; import { validateAndUpdateBlueprintMethodArgs } from './utils'; import NanoContractHeader from './header'; import leb128 from '../utils/leb128'; +import { NanoContractMethodArgument } from './methodArg'; class NanoContractTransactionBuilder { blueprintId: string | null | undefined; @@ -42,7 +42,7 @@ class NanoContractTransactionBuilder { caller: Buffer | null; - args: NanoContractArgumentType[] | null; + args: NanoContractArgumentApiInputType[] | null; transaction: Transaction | null; @@ -353,7 +353,8 @@ class NanoContractTransactionBuilder { } for (const [index, arg] of methodArgs.entries()) { - const serialized = serializer.serializeFromType(this.args[index], arg.type); + const methodArg = NanoContractMethodArgument.fromApiInput(arg.name, arg.type, this.args[index]); + const serialized = methodArg.serialize(serializer); serializedArgs.push(serialized); } } diff --git a/src/nano_contracts/deserializer.ts b/src/nano_contracts/deserializer.ts index e415ea3e2..b721a733f 100644 --- a/src/nano_contracts/deserializer.ts +++ b/src/nano_contracts/deserializer.ts @@ -5,11 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -import { bufferToHex, unpackToInt } from '../utils/buffer'; +import { unpackToInt } from '../utils/buffer'; import helpersUtils from '../utils/helpers'; import leb128Util from '../utils/leb128'; import Network from '../models/network'; -import { NanoContractArgumentType, BufferROExtract, NanoContractSignedData, NanoContractArgumentSingleType, NanoContractRawSignedData } from './types'; +import { + NanoContractArgumentType, + BufferROExtract, + NanoContractSignedData, + NanoContractArgumentSingleType, + NanoContractRawSignedData, +} from './types'; import { OutputValueType } from '../types'; import { NC_ARGS_MAX_BYTES_LENGTH } from '../constants'; import { getContainerInternalType, getContainerType } from './utils'; @@ -32,10 +38,7 @@ class Deserializer { * @memberof Deserializer * @inner */ - deserializeFromType( - buf: Buffer, - type: string - ): BufferROExtract { + deserializeFromType(buf: Buffer, type: string): BufferROExtract { const isContainerType = getContainerType(type) !== null; if (isContainerType) { return this.deserializeContainerType(buf, type); @@ -88,7 +91,6 @@ class Deserializer { /* eslint-disable class-methods-use-this -- XXX: Methods that don't use `this` should be made static */ - /** * Deserialize string value * @@ -291,14 +293,14 @@ class Deserializer { // Reading ContractId const ncIdResult = this.deserializeFromType(signedData, 'ContractId'); - const ncId = (ncIdResult.value as Buffer); + const ncId = ncIdResult.value as Buffer; const bytesReadFromContractId = ncIdResult.bytesRead; - let buf = signedData.subarray(bytesReadFromContractId); + const buf = signedData.subarray(bytesReadFromContractId); // Reading argument const parseResult = this.deserializeFromType(buf, type); - let parsed = parseResult.value; + const parsed = parseResult.value; const bytesReadFromValue = parseResult.bytesRead; // Reading signature as bytes @@ -335,7 +337,7 @@ class Deserializer { // Reading argument const parseResult = this.deserializeFromType(signedData, type); - let parsed = parseResult.value; + const parsed = parseResult.value; const bytesReadFromValue = parseResult.bytesRead; // Reading signature @@ -364,7 +366,7 @@ class Deserializer { */ toTuple(buf: Buffer, type: string): BufferROExtract> { const typeArr = type.split(',').map(s => s.trim()); - const tupleValues: NanoContractArgumentType[] = [] + const tupleValues: NanoContractArgumentType[] = []; let bytesReadTotal = 0; let tupleBuf = buf.subarray(); for (const t of typeArr) { @@ -376,7 +378,7 @@ class Deserializer { return { value: tupleValues, bytesRead: bytesReadTotal, - } + }; } } diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index 6a89857e5..16cf5405a 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -12,14 +12,19 @@ import { NanoContractRawSignedData, NanoContractSignedData, BufferROExtract, + NanoContractArgumentApiInputType, } from './types'; import Serializer from './serializer'; import Deserializer from './deserializer'; +import { getContainerInternalType, getContainerType } from './utils'; export class NanoContractMethodArgument { name: string; + type: string; + value: NanoContractArgumentType; + _serialized: Buffer; constructor(name: string, type: string, value: NanoContractArgumentType) { @@ -31,23 +36,121 @@ export class NanoContractMethodArgument { serialize(serializer: Serializer): Buffer { if (this._serialized.length === 0) { - this._serialized = serializer.serializeFromType(this.value, this.type) + this._serialized = serializer.serializeFromType(this.value, this.type); } - return this._serialized + return this._serialized; } - static fromSerialized(name: string, type: string, buf: Buffer, deserializer: Deserializer): BufferROExtract { + static fromSerialized( + name: string, + type: string, + buf: Buffer, + deserializer: Deserializer + ): BufferROExtract { const parseResult = deserializer.deserializeFromType(buf, type); return { value: new NanoContractMethodArgument(name, type, parseResult.value), bytesRead: parseResult.bytesRead, + }; + } + + /** + * User input and api serialized input may not be encoded in the actual value type. + * + * ## SignedData + * We expect the value as a string separated by comma (,) with 4 elements + * (signature, ncID, value, type) + * Since the value is encoded as a string some special cases apply: + * - bool: 'true' or 'false'. + * - bytes (and any bytes encoded value): hex encoded string of the byte value. + * + * While the value should be the NanoContractSignedDataSchema + * + * ## RawSignedData + * We expect the value as a string separated by comma (,) with 3 elements + * (signature, value, type) + * + * While the value should be the NanoContractRawSignedDataSchema + */ + static fromApiInput(name: string, type: string, value: NanoContractArgumentApiInputType): NanoContractMethodArgument { + const isContainerType = getContainerType(type) !== null; + if (isContainerType) { + const [containerType, innerType] = getContainerInternalType(type); + if (containerType === 'SignedData') { + // Parse string SignedData into NanoContractSignedData + const splittedValue = (value as string).split(','); + if (splittedValue.length != 4) { + throw new Error(); + } + const [signature, ncId, val, valType] = splittedValue; + if (valType.trim() != innerType.trim()) { + throw new Error(); + } + + let finalValue: NanoContractArgumentSingleType = val; + if (innerType === 'bytes') { + finalValue = Buffer.from(val, 'hex'); + } else if (innerType === 'bool') { + // If the result is expected as boolean, it will come here as a string true/false + finalValue = val === 'true'; + } else if (innerType === 'int') { + finalValue = Number.parseInt(val, 10); + } else if (innerType === 'VarInt') { + finalValue = BigInt(val); + } else { + // For the other types + finalValue = val; + } + + const data: NanoContractSignedData = { + type: innerType, + value: [Buffer.from(ncId, 'hex'), finalValue], + signature: Buffer.from(signature, 'hex'), + } + return new NanoContractMethodArgument(name, type, data); + } else if (containerType === 'RawSignedData') { + // Parse string RawSignedData into NanoContractRawSignedData + const splittedValue = (value as string).split(','); + if (splittedValue.length != 3) { + throw new Error(); + } + const [signature, val, valType] = splittedValue; + if (valType.trim() != innerType.trim()) { + throw new Error(); + } + + let finalValue: NanoContractArgumentSingleType = val; + if (innerType === 'bytes') { + finalValue = Buffer.from(val, 'hex'); + } else if (innerType === 'bool') { + // If the result is expected as boolean, it will come here as a string true/false + finalValue = val === 'true'; + } else if (innerType === 'int') { + value = Number.parseInt(val, 10); + } else if (innerType === 'VarInt') { + value = BigInt(val); + } else { + // For the other types + value = val; + } + + const data: NanoContractRawSignedData = { + type: innerType, + value: finalValue, + signature: Buffer.from(signature, 'hex'), + } + return new NanoContractMethodArgument(name, type, data); + } + // XXX: Should we have a special case for Optional, Tuple? } + + return new NanoContractMethodArgument(name, type, value); } - toHumanReadable(): NanoContractParsedArgument { + toApiInput(): NanoContractParsedArgument { function prepSingleValue(type: string, value: NanoContractArgumentSingleType) { - switch(type) { + switch (type) { case 'bytes': return (value as Buffer).toString('hex'); case 'bool': @@ -56,7 +159,7 @@ export class NanoContractMethodArgument { return value; } } - + if (this.type.startsWith('SignedData')) { const data = this.value as NanoContractSignedData; return { @@ -68,7 +171,7 @@ export class NanoContractMethodArgument { prepSingleValue(data.type, data.value[1]), this.type, ].join(','), - } + }; } if (this.type.startsWith('RawSignedData')) { @@ -81,13 +184,13 @@ export class NanoContractMethodArgument { prepSingleValue(data.type, data.value), this.type, ].join(','), - } + }; } - + return { name: this.name, type: this.type, parsed: this.value, - } + }; } } diff --git a/src/nano_contracts/parser.ts b/src/nano_contracts/parser.ts index 516641494..9878481f4 100644 --- a/src/nano_contracts/parser.ts +++ b/src/nano_contracts/parser.ts @@ -103,7 +103,7 @@ class NanoContractTransactionParser { arg.name, arg.type, argsBuffer, - deserializer, + deserializer ); parsed = parseResult.value; size = parseResult.bytesRead; diff --git a/src/nano_contracts/serializer.ts b/src/nano_contracts/serializer.ts index 170ad2420..fa8da4979 100644 --- a/src/nano_contracts/serializer.ts +++ b/src/nano_contracts/serializer.ts @@ -7,13 +7,11 @@ import Address from '../models/address'; import Network from '../models/network'; -import { hexToBuffer, intToBytes, signedIntToBytes, bigIntToBytes } from '../utils/buffer'; -import { NanoContractArgumentType } from './types'; +import { signedIntToBytes, bigIntToBytes } from '../utils/buffer'; +import { NanoContractArgumentType, NanoContractRawSignedData, NanoContractSignedData } from './types'; import { OutputValueType } from '../types'; import leb128Util from '../utils/leb128'; - -// Number of bytes used to serialize the size of the value -const SERIALIZATION_SIZE_LEN = 2; +import { getContainerInternalType, getContainerType } from './utils'; /* eslint-disable class-methods-use-this -- XXX: Methods that do not use `this` should be made static */ class Serializer { @@ -23,41 +21,21 @@ class Serializer { this.network = network; } - /** - * Push an integer to buffer as the len of serialized element - * Use SERIALIZATION_SIZE_LEN as the quantity of bytes to serialize - * the integer - * - * @param {buf} Array of buffer to push the serialized integer - * @param {len} Integer to serialize - * - * @memberof Serializer - * @inner - */ - pushLenValue(buf: Buffer[], len: number) { - buf.push(intToBytes(len, SERIALIZATION_SIZE_LEN)); - } - /** * Helper method to serialize any value from its type * We receive these type from the full node, so we * use the python syntax * - * @param {value} Value to serialize - * @param {type} Type of the value to be serialized + * @param value Value to serialize + * @param type Type of the value to be serialized * * @memberof Serializer * @inner */ serializeFromType(value: NanoContractArgumentType, type: string): Buffer { - if (type.endsWith('?')) { - // This is an optional - const optionalType = type.slice(0, -1); - return this.fromOptional(value, optionalType); - } - - if (type.startsWith('SignedData[')) { - return this.fromSigned(value as string); + const isContainerType = getContainerType(type) !== null; + if (isContainerType) { + return this.serializeContainerType(value, type); } switch (type) { @@ -86,6 +64,22 @@ class Serializer { } } + serializeContainerType(value: NanoContractArgumentType, type: string) { + const [containerType, innerType] = getContainerInternalType(type); + + switch (containerType) { + case 'Optional': + return this.fromOptional(value, innerType); + case 'SignedData': + return this.fromSignedData(value as NanoContractSignedData, innerType); + case 'RawSignedData': + case 'Tuple': + throw new Error('Not implemented'); + default: + throw new Error('Invalid type'); + } + } + /** * Serialize string value. * - length (leb128 integer) @@ -110,7 +104,7 @@ class Serializer { * @inner */ fromAddress(value: string): Buffer { - const address = new Address(value, { network: this.network}); + const address = new Address(value, { network: this.network }); address.validateAddress(); return this.fromBytes(address.decode()); } @@ -168,6 +162,19 @@ class Serializer { return Buffer.from([0]); } + /** + * Serialize a bigint value as a variable length integer. + * The serialization will use leb128. + * + * @param {bigint} value + * + * @memberof Serializer + */ + fromVarInt(value: bigint): Buffer { + return leb128Util.encodeSigned(value); + } + /* eslint-disable class-methods-use-this */ + /** * Serialize an optional value * @@ -209,53 +216,49 @@ class Serializer { * @memberof Serializer * @inner */ - fromSigned(signedValue: string): Buffer { - const splittedValue = signedValue.split(','); - if (splittedValue.length !== 3) { - throw new Error('Signed data requires 3 parameters.'); - } - // First value must be a Buffer but comes as hex - const inputData = hexToBuffer(splittedValue[0]); - const type = splittedValue[2]; - let value: Buffer | string | boolean | number | bigint; - if (type === 'bytes') { - // If the result is expected as bytes, it will come here in the args as hex value - value = hexToBuffer(splittedValue[1]); - } else if (type === 'bool') { - // If the result is expected as boolean, it will come here as a string true/false - value = splittedValue[1] === 'true'; - } else if (type === 'int') { - value = Number.parseInt(splittedValue[1], 10); - } else if (type === 'VarInt') { - value = BigInt(splittedValue[1]); - } else { - // For the other types - // eslint-disable-next-line prefer-destructuring - value = splittedValue[1]; - } - + fromSignedData(signedValue: NanoContractSignedData, type: string): Buffer { const ret: Buffer[] = []; + if (signedValue.type !== type) { + throw new Error('type mismatch'); + } - const serialized = this.serializeFromType(value, type); + const ncId = this.serializeFromType(signedValue.value[0], 'bytes'); + ret.push(ncId); + const serialized = this.serializeFromType(signedValue.value[1], signedValue.type); ret.push(serialized); - const signature = this.serializeFromType(inputData, 'bytes'); + const signature = this.serializeFromType(signedValue.signature, 'bytes'); ret.push(signature); return Buffer.concat(ret); } + /** - * Serialize a bigint value as a variable length integer. - * The serialization will use leb128. + * Serialize a signed value + * We expect the value as a string separated by comma (,) + * with 3 elements (inputData, value, type) * - * @param {bigint} value + * The serialization will be + * [len(serializedValue)][serializedValue][inputData] + * + * @param signedValue String value with inputData, value, and type separated by comma * * @memberof Serializer + * @inner */ - fromVarInt(value: bigint): Buffer { - return leb128Util.encodeSigned(value); + fromRawSignedData(signedValue: NanoContractRawSignedData, type: string): Buffer { + const ret: Buffer[] = []; + if (signedValue.type !== type) { + throw new Error('type mismatch'); + } + + const serialized = this.serializeFromType(signedValue.value, signedValue.type); + ret.push(serialized); + const signature = this.serializeFromType(signedValue.signature, 'bytes'); + ret.push(signature); + + return Buffer.concat(ret); } } -/* eslint-disable class-methods-use-this */ export default Serializer; diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 01fda5461..1c16ff8c0 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -5,24 +5,27 @@ * LICENSE file in the root directory of this source tree. */ import { IHistoryTx, OutputValueType } from '../types'; +import { z } from 'zod'; /** * There are the types that can be received via api * when querying for a nano contract value. */ -export type NanoContractArgumentApiInputType = - | string - | number - | bigint - | OutputValueType - | boolean - | null; +export const NanoContractArgumentApiInputSchema = z.union([ + z.string(), + z.number(), + z.bigint(), + z.boolean(), + z.null(), +]); +export type NanoContractArgumentApiInputType = z.output; /** * These are the possible `Single` types after parsing * We include Buffer since some types are decoded as Buffer (e.g. bytes, TokenUid, ContractId) */ -export type NanoContractArgumentSingleType = NanoContractArgumentApiInputType | Buffer; +export const NanoContractArgumentSingleSchema = z.union([NanoContractArgumentApiInputSchema, z.instanceof(Buffer)]); +export type NanoContractArgumentSingleType = z.output; /** * A `SignedData` value is the tuple `[ContractId, Value]` @@ -33,36 +36,43 @@ export type NanoContractSignedDataInnerType = [Buffer, NanoContractArgumentSingl /** * NanoContract SignedData method argument type */ -export type NanoContractSignedData = { - type: string; - value: NanoContractSignedDataInnerType; - signature: Buffer; -} +export const NanoContractSignedDataSchema = z.object({ + type: z.string(), + signature: z.instanceof(Buffer), + value: z.tuple([z.instanceof(Buffer), NanoContractArgumentSingleSchema]), +}); +export type NanoContractSignedData = z.output; /** * NanoContract RawSignedData method argument type */ -export type NanoContractRawSignedData = { - type: string; - value: NanoContractArgumentSingleType; - signature: Buffer; -} +export const NanoContractRawSignedDataSchema = z.object({ + type: z.string(), + signature: z.instanceof(Buffer), + value: NanoContractArgumentSingleSchema, +}); +export type NanoContractRawSignedData = z.output; /** - * Intermediate type for all possible Nano contract argument type + * Intermediate schema for all possible Nano contract argument type * that do not include tuple/arrays/repetition */ -type _NanoContractArgumentType1 = NanoContractArgumentSingleType | NanoContractSignedData | NanoContractRawSignedData; +const _NanoContractArgumentType1Schema = z.union([NanoContractArgumentSingleSchema, NanoContractSignedDataSchema, NanoContractRawSignedDataSchema]) /** * Nano Contract method argument type as a native TS type */ -export type NanoContractArgumentType = _NanoContractArgumentType1 | _NanoContractArgumentType1[]; +export const NanoContractArgumentSchema = z.union([_NanoContractArgumentType1Schema, z.array(_NanoContractArgumentType1Schema)]); +export type NanoContractArgumentType = z.output; /** * Container type names */ -export type NanoContractArgumentContainerType = 'Optional' | 'SignedData' | 'RawSignedData' | 'Tuple'; +export type NanoContractArgumentContainerType = + | 'Optional' + | 'SignedData' + | 'RawSignedData' + | 'Tuple'; export enum NanoContractActionType { DEPOSIT = 'deposit', @@ -188,4 +198,4 @@ export interface NanoContractStateAPIParameters { export type BufferROExtract = { value: T; bytesRead: number; -} +}; diff --git a/src/nano_contracts/utils.ts b/src/nano_contracts/utils.ts index ae82771d0..5c837b60f 100644 --- a/src/nano_contracts/utils.ts +++ b/src/nano_contracts/utils.ts @@ -41,7 +41,7 @@ export function getContainerInternalType( const match = type.match(/^(.*?)\[(.*)\]/); const containerType = match ? match[1] : null; const internalType = match ? match[2] : null; - if ((!internalType) || (!containerType)) { + if (!internalType || !containerType) { throw new Error('Unable to extract type'); } // Only some values are allowed for containerType @@ -49,7 +49,7 @@ export function getContainerInternalType( case 'Tuple': case 'SignedData': case 'RawSignedData': - return [containerType, internalType] + return [containerType, internalType]; default: throw new Error('Not a ContainerType'); } @@ -60,11 +60,7 @@ export function getContainerType(type: string): NanoContractArgumentContainerTyp const [containerType, _internalType] = getContainerInternalType(type); return containerType; } catch (err: unknown) { - if (err instanceof Error && err.message === 'Not a ContainerType') { - return null; - } - // Re-raise unexpected error - throw err; + return null; } } From dbb40cf6000e1486ed248b146e285e45950aaf6e Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 00:47:21 -0300 Subject: [PATCH 04/23] chore: linter changes --- __tests__/nano_contracts/deserializer.test.ts | 51 +++++-- __tests__/nano_contracts/methodArg.test.ts | 106 ++++++++++---- __tests__/nano_contracts/serializer.test.ts | 138 ++++++++++-------- src/nano_contracts/builder.ts | 6 +- src/nano_contracts/deserializer.ts | 2 +- src/nano_contracts/methodArg.ts | 27 ++-- src/nano_contracts/parser.ts | 3 +- src/nano_contracts/serializer.ts | 7 +- src/nano_contracts/types.ts | 20 ++- 9 files changed, 231 insertions(+), 129 deletions(-) diff --git a/__tests__/nano_contracts/deserializer.test.ts b/__tests__/nano_contracts/deserializer.test.ts index 3ca019afa..806504f6f 100644 --- a/__tests__/nano_contracts/deserializer.test.ts +++ b/__tests__/nano_contracts/deserializer.test.ts @@ -235,7 +235,9 @@ test('SignedData', () => { expect((deserializedBytes as NanoContractSignedData).type).toEqual(valueBytes.type); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBytes as NanoContractSignedData).signature).toMatchBuffer(valueBytes.signature); + expect((deserializedBytes as NanoContractSignedData).signature).toMatchBuffer( + valueBytes.signature + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((deserializedBytes as NanoContractSignedData).value[0]).toMatchBuffer(valueBytes.value[0]); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. @@ -254,10 +256,16 @@ test('SignedData', () => { expect((deserializedBoolFalse as NanoContractSignedData).type).toEqual(valueBoolFalse.type); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBoolFalse as NanoContractSignedData).signature).toMatchBuffer(valueBoolFalse.signature); + expect((deserializedBoolFalse as NanoContractSignedData).signature).toMatchBuffer( + valueBoolFalse.signature + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBoolFalse as NanoContractSignedData).value[0]).toMatchBuffer(valueBoolFalse.value[0]); - expect((deserializedBoolFalse as NanoContractSignedData).value[1]).toEqual(valueBoolFalse.value[1]); + expect((deserializedBoolFalse as NanoContractSignedData).value[0]).toMatchBuffer( + valueBoolFalse.value[0] + ); + expect((deserializedBoolFalse as NanoContractSignedData).value[1]).toEqual( + valueBoolFalse.value[1] + ); const valueBoolTrue: NanoContractSignedData = { type: 'bool', @@ -272,9 +280,13 @@ test('SignedData', () => { expect((deserializedBoolTrue as NanoContractSignedData).type).toEqual(valueBoolTrue.type); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBoolTrue as NanoContractSignedData).signature).toMatchBuffer(valueBoolTrue.signature); + expect((deserializedBoolTrue as NanoContractSignedData).signature).toMatchBuffer( + valueBoolTrue.signature + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBoolTrue as NanoContractSignedData).value[0]).toMatchBuffer(valueBoolTrue.value[0]); + expect((deserializedBoolTrue as NanoContractSignedData).value[0]).toMatchBuffer( + valueBoolTrue.value[0] + ); expect((deserializedBoolTrue as NanoContractSignedData).value[1]).toEqual(valueBoolTrue.value[1]); const valueVarInt: NanoContractSignedData = { @@ -290,9 +302,13 @@ test('SignedData', () => { expect((deserializedVarInt as NanoContractSignedData).type).toEqual(valueVarInt.type); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedVarInt as NanoContractSignedData).signature).toMatchBuffer(valueVarInt.signature); + expect((deserializedVarInt as NanoContractSignedData).signature).toMatchBuffer( + valueVarInt.signature + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedVarInt as NanoContractSignedData).value[0]).toMatchBuffer(valueVarInt.value[0]); + expect((deserializedVarInt as NanoContractSignedData).value[0]).toMatchBuffer( + valueVarInt.value[0] + ); expect((deserializedVarInt as NanoContractSignedData).value[1]).toEqual(valueVarInt.value[1]); }); @@ -305,19 +321,22 @@ test('Address', () => { const { value: deserialized } = deserializer.deserializeFromType( Buffer.concat([leb128.encodeUnsigned(addressBuffer.length), addressBuffer]), - 'Address'); + 'Address' + ); expect(deserialized).toBe(address); const wrongNetworkAddress = 'HDeadDeadDeadDeadDeadDeadDeagTPgmn'; const wrongNetworkAddressBuffer = new Address(wrongNetworkAddress).decode(); - expect(() => deserializer.deserializeFromType( - Buffer.concat([ - leb128.encodeUnsigned(wrongNetworkAddressBuffer.length), - wrongNetworkAddressBuffer, - ]), - 'Address', - )).toThrow(); + expect(() => + deserializer.deserializeFromType( + Buffer.concat([ + leb128.encodeUnsigned(wrongNetworkAddressBuffer.length), + wrongNetworkAddressBuffer, + ]), + 'Address' + ) + ).toThrow(); }); test('VarInt', () => { diff --git a/__tests__/nano_contracts/methodArg.test.ts b/__tests__/nano_contracts/methodArg.test.ts index cba0f0b98..ea6f16bb5 100644 --- a/__tests__/nano_contracts/methodArg.test.ts +++ b/__tests__/nano_contracts/methodArg.test.ts @@ -5,12 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -import { NanoContractMethodArgument } from "../../src/nano_contracts/methodArg"; -import { NanoContractSignedData } from "../../src/nano_contracts/types"; +import { NanoContractMethodArgument } from '../../src/nano_contracts/methodArg'; +import { NanoContractSignedData } from '../../src/nano_contracts/types'; describe('fromApiInput', () => { it('should read SignedData[int]', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[int]', '74657374,6e634944,300,int') + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'SignedData[int]', + '74657374,6e634944,300,int' + ); expect(arg).toMatchObject({ name: 'a-test', type: 'SignedData[int]', @@ -18,17 +22,25 @@ describe('fromApiInput', () => { type: 'int', signature: expect.anything(), value: expect.anything(), - } + }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( + Buffer.from([0x74, 0x65, 0x73, 0x74]) + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + Buffer.from([0x6e, 0x63, 0x49, 0x44]) + ); expect((arg.value as NanoContractSignedData).value[1]).toEqual(300); }); it('should read SignedData[VarInt]', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[VarInt]', '74657374,6e634944,300,VarInt') + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'SignedData[VarInt]', + '74657374,6e634944,300,VarInt' + ); expect(arg).toMatchObject({ name: 'a-test', type: 'SignedData[VarInt]', @@ -36,17 +48,25 @@ describe('fromApiInput', () => { type: 'VarInt', signature: expect.anything(), value: expect.anything(), - } + }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( + Buffer.from([0x74, 0x65, 0x73, 0x74]) + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + Buffer.from([0x6e, 0x63, 0x49, 0x44]) + ); expect((arg.value as NanoContractSignedData).value[1]).toEqual(300n); }); it('should read SignedData[str]', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[str]', '74657374,6e634944,test,str') + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'SignedData[str]', + '74657374,6e634944,test,str' + ); expect(arg).toMatchObject({ name: 'a-test', type: 'SignedData[str]', @@ -54,17 +74,25 @@ describe('fromApiInput', () => { type: 'str', signature: expect.anything(), value: expect.anything(), - } + }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( + Buffer.from([0x74, 0x65, 0x73, 0x74]) + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + Buffer.from([0x6e, 0x63, 0x49, 0x44]) + ); expect((arg.value as NanoContractSignedData).value[1]).toEqual('test'); }); it('should read SignedData[bytes]', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[bytes]', '74657374,6e634944,74657374,bytes') + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'SignedData[bytes]', + '74657374,6e634944,74657374,bytes' + ); expect(arg).toMatchObject({ name: 'a-test', type: 'SignedData[bytes]', @@ -72,19 +100,28 @@ describe('fromApiInput', () => { type: 'bytes', signature: expect.anything(), value: expect.anything(), - } + }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( + Buffer.from([0x74, 0x65, 0x73, 0x74]) + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + Buffer.from([0x6e, 0x63, 0x49, 0x44]) + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[1]).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + expect((arg.value as NanoContractSignedData).value[1]).toMatchBuffer( + Buffer.from([0x74, 0x65, 0x73, 0x74]) + ); }); - it('should read true SignedData[bool]', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[bool]', '74657374,6e634944,true,bool') + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'SignedData[bool]', + '74657374,6e634944,true,bool' + ); expect(arg).toMatchObject({ name: 'a-test', type: 'SignedData[bool]', @@ -92,18 +129,25 @@ describe('fromApiInput', () => { type: 'bool', signature: expect.anything(), value: expect.anything(), - } + }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( + Buffer.from([0x74, 0x65, 0x73, 0x74]) + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + Buffer.from([0x6e, 0x63, 0x49, 0x44]) + ); expect((arg.value as NanoContractSignedData).value[1]).toEqual(true); }); - it('should read false SignedData[bool]', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'SignedData[bool]', '74657374,6e634944,false,bool') + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'SignedData[bool]', + '74657374,6e634944,false,bool' + ); expect(arg).toMatchObject({ name: 'a-test', type: 'SignedData[bool]', @@ -111,12 +155,16 @@ describe('fromApiInput', () => { type: 'bool', signature: expect.anything(), value: expect.anything(), - } + }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).signature).toMatchBuffer(Buffer.from([0x74, 0x65, 0x73, 0x74])); + expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( + Buffer.from([0x74, 0x65, 0x73, 0x74]) + ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from([0x6e, 0x63, 0x49, 0x44])); + expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + Buffer.from([0x6e, 0x63, 0x49, 0x44]) + ); expect((arg.value as NanoContractSignedData).value[1]).toEqual(false); }); }); diff --git a/__tests__/nano_contracts/serializer.test.ts b/__tests__/nano_contracts/serializer.test.ts index a05444e82..24342a430 100644 --- a/__tests__/nano_contracts/serializer.test.ts +++ b/__tests__/nano_contracts/serializer.test.ts @@ -89,78 +89,92 @@ test('Optional', () => { test('SignedData', () => { const serializer = new Serializer(new Network('testnet')); - expect(serializer.fromSignedData( - { - signature: Buffer.from('74657374', 'hex'), - type: 'int', - value: [Buffer.from('6e634944', 'hex'), 300], - }, - 'int', - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - )).toMatchBuffer( + expect( + serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'int', + value: [Buffer.from('6e634944', 'hex'), 300], + }, + 'int' + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ) + ).toMatchBuffer( // 4 + ncId + value + 4 + test - Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x00, 0x00, 0x01, 0x2c, 0x04, 0x74, 0x65, 0x73, 0x74]) + Buffer.from([ + 0x04, 0x6e, 0x63, 0x49, 0x44, 0x00, 0x00, 0x01, 0x2c, 0x04, 0x74, 0x65, 0x73, 0x74, + ]) ); - expect(serializer.fromSignedData( - { - signature: Buffer.from('74657374', 'hex'), - type: 'VarInt', - value: [Buffer.from('6e634944', 'hex'), 300n], - }, - 'VarInt', - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - )).toMatchBuffer( + expect( + serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'VarInt', + value: [Buffer.from('6e634944', 'hex'), 300n], + }, + 'VarInt' + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ) + ).toMatchBuffer( Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0xac, 0x02, 0x04, 0x74, 0x65, 0x73, 0x74]) ); - expect(serializer.fromSignedData( - { - signature: Buffer.from('74657374', 'hex'), - type: 'str', - value: [Buffer.from('6e634944', 'hex'), 'test'], - }, - 'str', - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - )).toMatchBuffer( - Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74]) - ); - - expect(serializer.fromSignedData( - { - signature: Buffer.from('74657374', 'hex'), - type: 'bytes', - value: [Buffer.from('6e634944', 'hex'), Buffer.from('74657374', 'hex')], - }, - 'bytes', - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - )).toMatchBuffer( - Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74]) + expect( + serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'str', + value: [Buffer.from('6e634944', 'hex'), 'test'], + }, + 'str' + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ) + ).toMatchBuffer( + Buffer.from([ + 0x04, 0x6e, 0x63, 0x49, 0x44, 0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74, + ]) ); - expect(serializer.fromSignedData( - { - signature: Buffer.from('74657374', 'hex'), - type: 'bool', - value: [Buffer.from('6e634944', 'hex'), false], - }, - 'bool', - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - )).toMatchBuffer( - Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74]) + expect( + serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'bytes', + value: [Buffer.from('6e634944', 'hex'), Buffer.from('74657374', 'hex')], + }, + 'bytes' + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ) + ).toMatchBuffer( + Buffer.from([ + 0x04, 0x6e, 0x63, 0x49, 0x44, 0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74, + ]) ); - expect(serializer.fromSignedData( - { - signature: Buffer.from('74657374', 'hex'), - type: 'bool', - value: [Buffer.from('6e634944', 'hex'), true], - }, - 'bool', - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - )).toMatchBuffer( - Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74]) - ); + expect( + serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'bool', + value: [Buffer.from('6e634944', 'hex'), false], + }, + 'bool' + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ) + ).toMatchBuffer(Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74])); + + expect( + serializer.fromSignedData( + { + signature: Buffer.from('74657374', 'hex'), + type: 'bool', + value: [Buffer.from('6e634944', 'hex'), true], + }, + 'bool' + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ) + ).toMatchBuffer(Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74])); }); test('VarInt', () => { diff --git a/src/nano_contracts/builder.ts b/src/nano_contracts/builder.ts index ea59e4f1c..c9f01ad85 100644 --- a/src/nano_contracts/builder.ts +++ b/src/nano_contracts/builder.ts @@ -353,7 +353,11 @@ class NanoContractTransactionBuilder { } for (const [index, arg] of methodArgs.entries()) { - const methodArg = NanoContractMethodArgument.fromApiInput(arg.name, arg.type, this.args[index]); + const methodArg = NanoContractMethodArgument.fromApiInput( + arg.name, + arg.type, + this.args[index] + ); const serialized = methodArg.serialize(serializer); serializedArgs.push(serialized); } diff --git a/src/nano_contracts/deserializer.ts b/src/nano_contracts/deserializer.ts index b721a733f..6323dfd09 100644 --- a/src/nano_contracts/deserializer.ts +++ b/src/nano_contracts/deserializer.ts @@ -364,7 +364,7 @@ class Deserializer { * @memberof Deserializer * @inner */ - toTuple(buf: Buffer, type: string): BufferROExtract> { + toTuple(buf: Buffer, type: string): BufferROExtract> { const typeArr = type.split(',').map(s => s.trim()); const tupleValues: NanoContractArgumentType[] = []; let bytesReadTotal = 0; diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index 16cf5405a..7e3f3ce83 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -73,18 +73,22 @@ export class NanoContractMethodArgument { * * While the value should be the NanoContractRawSignedDataSchema */ - static fromApiInput(name: string, type: string, value: NanoContractArgumentApiInputType): NanoContractMethodArgument { + static fromApiInput( + name: string, + type: string, + value: NanoContractArgumentApiInputType + ): NanoContractMethodArgument { const isContainerType = getContainerType(type) !== null; if (isContainerType) { const [containerType, innerType] = getContainerInternalType(type); if (containerType === 'SignedData') { // Parse string SignedData into NanoContractSignedData const splittedValue = (value as string).split(','); - if (splittedValue.length != 4) { + if (splittedValue.length !== 4) { throw new Error(); } const [signature, ncId, val, valType] = splittedValue; - if (valType.trim() != innerType.trim()) { + if (valType.trim() !== innerType.trim()) { throw new Error(); } @@ -107,16 +111,17 @@ export class NanoContractMethodArgument { type: innerType, value: [Buffer.from(ncId, 'hex'), finalValue], signature: Buffer.from(signature, 'hex'), - } + }; return new NanoContractMethodArgument(name, type, data); - } else if (containerType === 'RawSignedData') { + } + if (containerType === 'RawSignedData') { // Parse string RawSignedData into NanoContractRawSignedData const splittedValue = (value as string).split(','); - if (splittedValue.length != 3) { + if (splittedValue.length !== 3) { throw new Error(); } const [signature, val, valType] = splittedValue; - if (valType.trim() != innerType.trim()) { + if (valType.trim() !== innerType.trim()) { throw new Error(); } @@ -127,19 +132,19 @@ export class NanoContractMethodArgument { // If the result is expected as boolean, it will come here as a string true/false finalValue = val === 'true'; } else if (innerType === 'int') { - value = Number.parseInt(val, 10); + finalValue = Number.parseInt(val, 10); } else if (innerType === 'VarInt') { - value = BigInt(val); + finalValue = BigInt(val); } else { // For the other types - value = val; + finalValue = val; } const data: NanoContractRawSignedData = { type: innerType, value: finalValue, signature: Buffer.from(signature, 'hex'), - } + }; return new NanoContractMethodArgument(name, type, data); } // XXX: Should we have a special case for Optional, Tuple? diff --git a/src/nano_contracts/parser.ts b/src/nano_contracts/parser.ts index 9878481f4..d171403d4 100644 --- a/src/nano_contracts/parser.ts +++ b/src/nano_contracts/parser.ts @@ -12,7 +12,7 @@ import Deserializer from './deserializer'; import ncApi from '../api/nano'; import { getAddressFromPubkey } from '../utils/address'; import { NanoContractTransactionParseError } from '../errors'; -import { MethodArgInfo, NanoContractArgumentType, NanoContractParsedArgument } from './types'; +import { MethodArgInfo } from './types'; import leb128 from '../utils/leb128'; import { NanoContractMethodArgument } from './methodArg'; @@ -108,7 +108,6 @@ class NanoContractTransactionParser { parsed = parseResult.value; size = parseResult.bytesRead; } catch (err: unknown) { - console.error(err); throw new NanoContractTransactionParseError(`Failed to deserialize argument ${arg.type}.`); } parsedArgs.push(parsed); diff --git a/src/nano_contracts/serializer.ts b/src/nano_contracts/serializer.ts index fa8da4979..c3f795fe0 100644 --- a/src/nano_contracts/serializer.ts +++ b/src/nano_contracts/serializer.ts @@ -8,7 +8,11 @@ import Address from '../models/address'; import Network from '../models/network'; import { signedIntToBytes, bigIntToBytes } from '../utils/buffer'; -import { NanoContractArgumentType, NanoContractRawSignedData, NanoContractSignedData } from './types'; +import { + NanoContractArgumentType, + NanoContractRawSignedData, + NanoContractSignedData, +} from './types'; import { OutputValueType } from '../types'; import leb128Util from '../utils/leb128'; import { getContainerInternalType, getContainerType } from './utils'; @@ -232,7 +236,6 @@ class Serializer { return Buffer.concat(ret); } - /** * Serialize a signed value * We expect the value as a string separated by comma (,) diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 1c16ff8c0..38cee4773 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -4,8 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -import { IHistoryTx, OutputValueType } from '../types'; import { z } from 'zod'; +import { IHistoryTx, OutputValueType } from '../types'; /** * There are the types that can be received via api @@ -24,7 +24,10 @@ export type NanoContractArgumentApiInputType = z.output; /** @@ -57,12 +60,19 @@ export type NanoContractRawSignedData = z.output; /** @@ -195,7 +205,7 @@ export interface NanoContractStateAPIParameters { block_height?: number; } -export type BufferROExtract = { +export type BufferROExtract = { value: T; bytesRead: number; }; From c1d6531dfebbd3048abfadef716fd31abf80c991 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 12:06:35 -0300 Subject: [PATCH 05/23] tests(integration): fix expected values --- .../integration/nanocontracts/bet.test.ts | 106 +++++++++++------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/__tests__/integration/nanocontracts/bet.test.ts b/__tests__/integration/nanocontracts/bet.test.ts index dad9e5419..18252a69f 100644 --- a/__tests__/integration/nanocontracts/bet.test.ts +++ b/__tests__/integration/nanocontracts/bet.test.ts @@ -29,6 +29,7 @@ import { } from '../../../src/errors'; import { OutputType } from '../../../src/wallet/types'; import NanoContractTransactionParser from '../../../src/nano_contracts/parser'; +import { NanoContractSignedData } from '../../../src/nano_contracts/types'; let fundsTx; const builtInBlueprintId = '3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595'; @@ -120,11 +121,18 @@ describe('full cycle of bet nano contract', () => { tx1Parser.parseAddress(); await tx1Parser.parseArguments(); expect(tx1Parser.address?.base58).toBe(address0); - expect(tx1Parser.parsedArgs).toStrictEqual([ - { name: 'oracle_script', type: 'TxOutputScript', parsed: oracleData }, - { name: 'token_uid', type: 'TokenUid', parsed: Buffer.from(NATIVE_TOKEN_UID, 'hex') }, - { name: 'date_last_bet', type: 'Timestamp', parsed: dateLastBet }, - ]); + expect(tx1Parser.parsedArgs).not.toBeNull(); + if (tx1Parser.parsedArgs === null) { + throw new Error('Could not parse args'); + } + expect(tx1Parser.parsedArgs).toHaveLength(3); + expect(tx1Parser.parsedArgs[0]).toMatchObject({ name: 'oracle_script', type: 'TxOutputScript'}); + // @ts-expect-error + expect(tx1Parser.parsedArgs[0].value).toMatchBuffer(oracleData); + expect(tx1Parser.parsedArgs[1]).toMatchObject({ name: 'token_uid', type: 'TokenUid'}); + // @ts-expect-error + expect(tx1Parser.parsedArgs[1].value).toMatchBuffer(Buffer.from(NATIVE_TOKEN_UID, 'hex')); + expect(tx1Parser.parsedArgs[2]).toMatchObject({ name: 'date_last_bet', type: 'Timestamp', value: dateLastBet}); // First validate some bet arguments error handling const address2 = await wallet.getAddressAtIndex(2); @@ -196,10 +204,13 @@ describe('full cycle of bet nano contract', () => { txBetParser.parseAddress(); await txBetParser.parseArguments(); expect(txBetParser.address?.base58).toBe(address2); - expect(txBetParser.parsedArgs).toStrictEqual([ - { name: 'address', type: 'Address', parsed: address2 }, - { name: 'score', type: 'str', parsed: '1x0' }, - ]); + expect(txBetParser.parsedArgs).not.toBeNull(); + if (txBetParser.parsedArgs === null) { + throw new Error('Could not parse args'); + } + expect(txBetParser.parsedArgs).toHaveLength(2); + expect(txBetParser.parsedArgs[0]).toMatchObject({ name: 'address', type: 'Address', value: address2 }); + expect(txBetParser.parsedArgs[1]).toMatchObject({ name: 'score', type: 'str', value: '1x0'}); const utxos2 = await wallet.getUtxos(); // We must have one utxo in the address 0 of 900 HTR @@ -244,10 +255,13 @@ describe('full cycle of bet nano contract', () => { txBet2Parser.parseAddress(); await txBet2Parser.parseArguments(); expect(txBet2Parser.address?.base58).toBe(address3); - expect(txBet2Parser.parsedArgs).toStrictEqual([ - { name: 'address', type: 'Address', parsed: address3 }, - { name: 'score', type: 'str', parsed: '2x0' }, - ]); + expect(txBet2Parser.parsedArgs).not.toBeNull(); + if (txBet2Parser.parsedArgs === null) { + throw new Error('Could not parse args'); + } + expect(txBet2Parser.parsedArgs).toHaveLength(2); + expect(txBet2Parser.parsedArgs[0]).toMatchObject({ name: 'address', type: 'Address', value: address3 }); + expect(txBet2Parser.parsedArgs[1]).toMatchObject({ name: 'score', type: 'str', value: '2x0'}); // Get nc history const txIds = [tx1.hash, txBet.hash, txBet2.hash]; @@ -281,11 +295,11 @@ describe('full cycle of bet nano contract', () => { } const outputScriptBuffer1 = outputScript.createScript(); - expect(ncState.fields.token_uid.value).toBe(NATIVE_TOKEN_UID); - expect(ncState.fields.date_last_bet.value).toBe(dateLastBet); - expect(ncState.fields.oracle_script.value).toBe(bufferToHex(outputScriptBuffer1)); - expect(ncState.fields.final_result.value).toBeNull(); - expect(ncState.fields.total.value).toBe(300); + expect(ncState.fields['token_uid'].value).toBe(NATIVE_TOKEN_UID); + expect(ncState.fields['date_last_bet'].value).toBe(dateLastBet); + expect(ncState.fields['oracle_script'].value).toBe(bufferToHex(outputScriptBuffer1)); + expect(ncState.fields['final_result'].value).toBeNull(); + expect(ncState.fields['total'].value).toBe(300); expect(ncState.fields[`address_details.a'${address2}'`].value).toHaveProperty('1x0', 100); expect(ncState.fields[`withdrawals.a'${address2}'`].value).toBeUndefined(); expect(ncState.fields[`address_details.a'${address3}'`].value).toHaveProperty('2x0', 200); @@ -316,9 +330,9 @@ describe('full cycle of bet nano contract', () => { network, txSetResultData.tx.nc_args ); - txSetResultParser.parseAddress(network); + txSetResultParser.parseAddress(); await txSetResultParser.parseArguments(); - expect(txSetResultParser.address.base58).toBe(address1); + expect(txSetResultParser.address?.base58).toBe(address1); expect(txSetResultParser.parsedArgs).toStrictEqual([ { name: 'result', @@ -326,6 +340,16 @@ describe('full cycle of bet nano contract', () => { parsed: `${bufferToHex(inputData)},${result},str`, }, ]); + expect(txSetResultParser.parsedArgs).not.toBeNull(); + if (txSetResultParser.parsedArgs === null) { + throw new Error('Could not parse args'); + } + expect(txSetResultParser.parsedArgs).toHaveLength(1); + expect(txSetResultParser.parsedArgs[0]).toMatchObject({ name: 'result', type: 'SignedData[str]'}); + expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).type).toEqual('str'); + expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).signature).toMatchBuffer(inputData); + expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from(tx1.hash, 'hex')); + expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value[1]).toEqual(result); // Try to withdraw to address 2, success const txWithdrawal = await wallet.createAndSendNanoContractTransaction('withdraw', address2, { @@ -356,9 +380,9 @@ describe('full cycle of bet nano contract', () => { network, txWithdrawalData.tx.nc_args ); - txWithdrawalParser.parseAddress(network); + txWithdrawalParser.parseAddress(); await txWithdrawalParser.parseArguments(); - expect(txWithdrawalParser.address.base58).toBe(address2); + expect(txWithdrawalParser.address?.base58).toBe(address2); expect(txWithdrawalParser.parsedArgs).toBe(null); // Get state again @@ -373,11 +397,11 @@ describe('full cycle of bet nano contract', () => { `address_details.a'${address3}'`, `withdrawals.a'${address3}'`, ]); - expect(ncState2.fields.token_uid.value).toBe(NATIVE_TOKEN_UID); - expect(ncState2.fields.date_last_bet.value).toBe(dateLastBet); - expect(ncState2.fields.oracle_script.value).toBe(bufferToHex(outputScriptBuffer1)); - expect(ncState2.fields.final_result.value).toBe('1x0'); - expect(ncState2.fields.total.value).toBe(300); + expect(ncState2.fields['token_uid'].value).toBe(NATIVE_TOKEN_UID); + expect(ncState2.fields['date_last_bet'].value).toBe(dateLastBet); + expect(ncState2.fields['oracle_script'].value).toBe(bufferToHex(outputScriptBuffer1)); + expect(ncState2.fields['final_result'].value).toBe('1x0'); + expect(ncState2.fields['total'].value).toBe(300); expect(ncState2.fields[`address_details.a'${address2}'`].value).toHaveProperty('1x0', 100); expect(ncState2.fields[`withdrawals.a'${address2}'`].value).toBe(300); expect(ncState2.fields[`address_details.a'${address3}'`].value).toHaveProperty('2x0', 200); @@ -417,11 +441,11 @@ describe('full cycle of bet nano contract', () => { firstBlock ); - expect(ncStateFirstBlock.fields.token_uid.value).toBe(NATIVE_TOKEN_UID); - expect(ncStateFirstBlock.fields.date_last_bet.value).toBe(dateLastBet); - expect(ncStateFirstBlock.fields.oracle_script.value).toBe(bufferToHex(outputScriptBuffer1)); - expect(ncStateFirstBlock.fields.final_result.value).toBeNull(); - expect(ncStateFirstBlock.fields.total.value).toBe(300); + expect(ncStateFirstBlock.fields['token_uid'].value).toBe(NATIVE_TOKEN_UID); + expect(ncStateFirstBlock.fields['date_last_bet'].value).toBe(dateLastBet); + expect(ncStateFirstBlock.fields['oracle_script'].value).toBe(bufferToHex(outputScriptBuffer1)); + expect(ncStateFirstBlock.fields['final_result'].value).toBeNull(); + expect(ncStateFirstBlock.fields['total'].value).toBe(300); expect(ncStateFirstBlock.fields[`address_details.a'${address2}'`].value).toHaveProperty( '1x0', 100 @@ -453,13 +477,13 @@ describe('full cycle of bet nano contract', () => { firstBlockHeight ); - expect(ncStateFirstBlockHeight.fields.token_uid.value).toBe(NATIVE_TOKEN_UID); - expect(ncStateFirstBlockHeight.fields.date_last_bet.value).toBe(dateLastBet); - expect(ncStateFirstBlockHeight.fields.oracle_script.value).toBe( + expect(ncStateFirstBlockHeight.fields['token_uid'].value).toBe(NATIVE_TOKEN_UID); + expect(ncStateFirstBlockHeight.fields['date_last_bet'].value).toBe(dateLastBet); + expect(ncStateFirstBlockHeight.fields['oracle_script'].value).toBe( bufferToHex(outputScriptBuffer1) ); - expect(ncStateFirstBlockHeight.fields.final_result.value).toBeNull(); - expect(ncStateFirstBlockHeight.fields.total.value).toBe(300); + expect(ncStateFirstBlockHeight.fields['final_result'].value).toBeNull(); + expect(ncStateFirstBlockHeight.fields['total'].value).toBe(300); expect(ncStateFirstBlockHeight.fields[`address_details.a'${address2}'`].value).toHaveProperty( '1x0', 100 @@ -486,7 +510,7 @@ describe('full cycle of bet nano contract', () => { jest.spyOn(wallet.storage, 'processHistory'); expect(wallet.storage.processHistory.mock.calls.length).toBe(0); - await waitTxConfirmed(wallet, txWithdrawal2.hash); + await waitTxConfirmed(wallet, txWithdrawal2.hash, null); const txWithdrawal2Data = await wallet.getFullTxById(txWithdrawal2.hash); // The tx became voided after the block because of the nano execution @@ -519,16 +543,16 @@ describe('full cycle of bet nano contract', () => { // Add funds and validate address meta await GenesisWalletHelper.injectFunds(ocbWallet, address0, 1000n); const address0Meta = await ocbWallet.storage.store.getAddressMeta(address0); - expect(address0Meta.numTransactions).toBe(1); + expect(address0Meta?.numTransactions).toBe(1); // Use the bet blueprint code const code = fs.readFileSync('./__tests__/integration/configuration/bet.py', 'utf8'); const tx = await ocbWallet.createAndSendOnChainBlueprintTransaction(code, address10); // Wait for the tx to be confirmed, so we can use the on chain blueprint - await waitTxConfirmed(ocbWallet, tx.hash); + await waitTxConfirmed(ocbWallet, tx.hash!, null); // We must have one transaction in the address10 now const newAddress10Meta = await ocbWallet.storage.store.getAddressMeta(address10); - expect(newAddress10Meta.numTransactions).toBe(1); + expect(newAddress10Meta?.numTransactions).toBe(1); // Execute the bet blueprint tests await executeTests(ocbWallet, tx.hash); }); From 680823c79b89bdc81fce152036a1a3bdf8f4234b Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 12:10:59 -0300 Subject: [PATCH 06/23] chore: linter changes --- .../integration/nanocontracts/bet.test.ts | 92 ++++++++++++------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/__tests__/integration/nanocontracts/bet.test.ts b/__tests__/integration/nanocontracts/bet.test.ts index 18252a69f..8763f9656 100644 --- a/__tests__/integration/nanocontracts/bet.test.ts +++ b/__tests__/integration/nanocontracts/bet.test.ts @@ -126,13 +126,20 @@ describe('full cycle of bet nano contract', () => { throw new Error('Could not parse args'); } expect(tx1Parser.parsedArgs).toHaveLength(3); - expect(tx1Parser.parsedArgs[0]).toMatchObject({ name: 'oracle_script', type: 'TxOutputScript'}); - // @ts-expect-error + expect(tx1Parser.parsedArgs[0]).toMatchObject({ + name: 'oracle_script', + type: 'TxOutputScript', + }); + // @ts-expect-error toMatchBuffer is defined in setupTests.js expect(tx1Parser.parsedArgs[0].value).toMatchBuffer(oracleData); - expect(tx1Parser.parsedArgs[1]).toMatchObject({ name: 'token_uid', type: 'TokenUid'}); - // @ts-expect-error + expect(tx1Parser.parsedArgs[1]).toMatchObject({ name: 'token_uid', type: 'TokenUid' }); + // @ts-expect-error toMatchBuffer is defined in setupTests.js expect(tx1Parser.parsedArgs[1].value).toMatchBuffer(Buffer.from(NATIVE_TOKEN_UID, 'hex')); - expect(tx1Parser.parsedArgs[2]).toMatchObject({ name: 'date_last_bet', type: 'Timestamp', value: dateLastBet}); + expect(tx1Parser.parsedArgs[2]).toMatchObject({ + name: 'date_last_bet', + type: 'Timestamp', + value: dateLastBet, + }); // First validate some bet arguments error handling const address2 = await wallet.getAddressAtIndex(2); @@ -209,8 +216,12 @@ describe('full cycle of bet nano contract', () => { throw new Error('Could not parse args'); } expect(txBetParser.parsedArgs).toHaveLength(2); - expect(txBetParser.parsedArgs[0]).toMatchObject({ name: 'address', type: 'Address', value: address2 }); - expect(txBetParser.parsedArgs[1]).toMatchObject({ name: 'score', type: 'str', value: '1x0'}); + expect(txBetParser.parsedArgs[0]).toMatchObject({ + name: 'address', + type: 'Address', + value: address2, + }); + expect(txBetParser.parsedArgs[1]).toMatchObject({ name: 'score', type: 'str', value: '1x0' }); const utxos2 = await wallet.getUtxos(); // We must have one utxo in the address 0 of 900 HTR @@ -260,8 +271,12 @@ describe('full cycle of bet nano contract', () => { throw new Error('Could not parse args'); } expect(txBet2Parser.parsedArgs).toHaveLength(2); - expect(txBet2Parser.parsedArgs[0]).toMatchObject({ name: 'address', type: 'Address', value: address3 }); - expect(txBet2Parser.parsedArgs[1]).toMatchObject({ name: 'score', type: 'str', value: '2x0'}); + expect(txBet2Parser.parsedArgs[0]).toMatchObject({ + name: 'address', + type: 'Address', + value: address3, + }); + expect(txBet2Parser.parsedArgs[1]).toMatchObject({ name: 'score', type: 'str', value: '2x0' }); // Get nc history const txIds = [tx1.hash, txBet.hash, txBet2.hash]; @@ -295,11 +310,11 @@ describe('full cycle of bet nano contract', () => { } const outputScriptBuffer1 = outputScript.createScript(); - expect(ncState.fields['token_uid'].value).toBe(NATIVE_TOKEN_UID); - expect(ncState.fields['date_last_bet'].value).toBe(dateLastBet); - expect(ncState.fields['oracle_script'].value).toBe(bufferToHex(outputScriptBuffer1)); - expect(ncState.fields['final_result'].value).toBeNull(); - expect(ncState.fields['total'].value).toBe(300); + expect(ncState.fields.token_uid.value).toBe(NATIVE_TOKEN_UID); + expect(ncState.fields.date_last_bet.value).toBe(dateLastBet); + expect(ncState.fields.oracle_script.value).toBe(bufferToHex(outputScriptBuffer1)); + expect(ncState.fields.final_result.value).toBeNull(); + expect(ncState.fields.total.value).toBe(300); expect(ncState.fields[`address_details.a'${address2}'`].value).toHaveProperty('1x0', 100); expect(ncState.fields[`withdrawals.a'${address2}'`].value).toBeUndefined(); expect(ncState.fields[`address_details.a'${address3}'`].value).toHaveProperty('2x0', 200); @@ -345,11 +360,22 @@ describe('full cycle of bet nano contract', () => { throw new Error('Could not parse args'); } expect(txSetResultParser.parsedArgs).toHaveLength(1); - expect(txSetResultParser.parsedArgs[0]).toMatchObject({ name: 'result', type: 'SignedData[str]'}); + expect(txSetResultParser.parsedArgs[0]).toMatchObject({ + name: 'result', + type: 'SignedData[str]', + }); expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).type).toEqual('str'); - expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).signature).toMatchBuffer(inputData); - expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value[0]).toMatchBuffer(Buffer.from(tx1.hash, 'hex')); - expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value[1]).toEqual(result); + expect( + (txSetResultParser.parsedArgs[0].value as NanoContractSignedData).signature + // @ts-expect-error toMatchBuffer is defined in setupTests.js + ).toMatchBuffer(inputData); + expect( + (txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value[0] + // @ts-expect-error toMatchBuffer is defined in setupTests.js + ).toMatchBuffer(Buffer.from(tx1.hash, 'hex')); + expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value[1]).toEqual( + result + ); // Try to withdraw to address 2, success const txWithdrawal = await wallet.createAndSendNanoContractTransaction('withdraw', address2, { @@ -397,11 +423,11 @@ describe('full cycle of bet nano contract', () => { `address_details.a'${address3}'`, `withdrawals.a'${address3}'`, ]); - expect(ncState2.fields['token_uid'].value).toBe(NATIVE_TOKEN_UID); - expect(ncState2.fields['date_last_bet'].value).toBe(dateLastBet); - expect(ncState2.fields['oracle_script'].value).toBe(bufferToHex(outputScriptBuffer1)); - expect(ncState2.fields['final_result'].value).toBe('1x0'); - expect(ncState2.fields['total'].value).toBe(300); + expect(ncState2.fields.token_uid.value).toBe(NATIVE_TOKEN_UID); + expect(ncState2.fields.date_last_bet.value).toBe(dateLastBet); + expect(ncState2.fields.oracle_script.value).toBe(bufferToHex(outputScriptBuffer1)); + expect(ncState2.fields.final_result.value).toBe('1x0'); + expect(ncState2.fields.total.value).toBe(300); expect(ncState2.fields[`address_details.a'${address2}'`].value).toHaveProperty('1x0', 100); expect(ncState2.fields[`withdrawals.a'${address2}'`].value).toBe(300); expect(ncState2.fields[`address_details.a'${address3}'`].value).toHaveProperty('2x0', 200); @@ -441,11 +467,11 @@ describe('full cycle of bet nano contract', () => { firstBlock ); - expect(ncStateFirstBlock.fields['token_uid'].value).toBe(NATIVE_TOKEN_UID); - expect(ncStateFirstBlock.fields['date_last_bet'].value).toBe(dateLastBet); - expect(ncStateFirstBlock.fields['oracle_script'].value).toBe(bufferToHex(outputScriptBuffer1)); - expect(ncStateFirstBlock.fields['final_result'].value).toBeNull(); - expect(ncStateFirstBlock.fields['total'].value).toBe(300); + expect(ncStateFirstBlock.fields.token_uid.value).toBe(NATIVE_TOKEN_UID); + expect(ncStateFirstBlock.fields.date_last_bet.value).toBe(dateLastBet); + expect(ncStateFirstBlock.fields.oracle_script.value).toBe(bufferToHex(outputScriptBuffer1)); + expect(ncStateFirstBlock.fields.final_result.value).toBeNull(); + expect(ncStateFirstBlock.fields.total.value).toBe(300); expect(ncStateFirstBlock.fields[`address_details.a'${address2}'`].value).toHaveProperty( '1x0', 100 @@ -477,13 +503,13 @@ describe('full cycle of bet nano contract', () => { firstBlockHeight ); - expect(ncStateFirstBlockHeight.fields['token_uid'].value).toBe(NATIVE_TOKEN_UID); - expect(ncStateFirstBlockHeight.fields['date_last_bet'].value).toBe(dateLastBet); - expect(ncStateFirstBlockHeight.fields['oracle_script'].value).toBe( + expect(ncStateFirstBlockHeight.fields.token_uid.value).toBe(NATIVE_TOKEN_UID); + expect(ncStateFirstBlockHeight.fields.date_last_bet.value).toBe(dateLastBet); + expect(ncStateFirstBlockHeight.fields.oracle_script.value).toBe( bufferToHex(outputScriptBuffer1) ); - expect(ncStateFirstBlockHeight.fields['final_result'].value).toBeNull(); - expect(ncStateFirstBlockHeight.fields['total'].value).toBe(300); + expect(ncStateFirstBlockHeight.fields.final_result.value).toBeNull(); + expect(ncStateFirstBlockHeight.fields.total.value).toBe(300); expect(ncStateFirstBlockHeight.fields[`address_details.a'${address2}'`].value).toHaveProperty( '1x0', 100 From 1bf05cb84fc26f5c4e0cf8c6f0ebbdf1b7095149 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 12:30:38 -0300 Subject: [PATCH 07/23] feat: add toMatchBuffer into integration tests --- setupTests-integration.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/setupTests-integration.js b/setupTests-integration.js index 1eda662aa..909cd58ae 100644 --- a/setupTests-integration.js +++ b/setupTests-integration.js @@ -50,3 +50,25 @@ afterAll(async () => { // Storing data about used precalculated wallets for the next test suites await precalculationHelpers.test.storeDbIntoWalletsFile(); }); + +expect.extend({ + toMatchBuffer(received, expected) { + let pass; + if ((received instanceof Buffer === false) || (expected instanceof Buffer === false)) { + pass = false; + } else { + pass = expected.equals(received); + } + if (pass) { + return { + message: () => `expected Buffer(${received && received.toString('hex')}) to not match Buffer(${expected.toString('hex')})`, + pass: true, + } + } else { + return { + message: () => `expected Buffer(${received && received.toString('hex')}) to match Buffer(${expected.toString('hex')})`, + pass: false, + } + } + } +}); From 328d2a1e9745d041c0788372b54e655d73db50c4 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 16:06:31 -0300 Subject: [PATCH 08/23] feat: methodArgs completeness --- __tests__/nano_contracts/methodArg.test.ts | 150 +++++++++++++ src/nano_contracts/methodArg.ts | 250 +++++++++++++++------ src/nano_contracts/types.ts | 31 ++- src/nano_contracts/utils.ts | 1 + 4 files changed, 359 insertions(+), 73 deletions(-) diff --git a/__tests__/nano_contracts/methodArg.test.ts b/__tests__/nano_contracts/methodArg.test.ts index ea6f16bb5..bc074e1e9 100644 --- a/__tests__/nano_contracts/methodArg.test.ts +++ b/__tests__/nano_contracts/methodArg.test.ts @@ -167,4 +167,154 @@ describe('fromApiInput', () => { ); expect((arg.value as NanoContractSignedData).value[1]).toEqual(false); }); + + it('should read str values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'str', 'test'); + expect(arg).toMatchObject({ name: 'a-test', type: 'str', value: 'test' }); + }); + + it('should read bytes values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'bytes', '74657374'); + expect(arg).toMatchObject({ name: 'a-test', type: 'bytes', value: expect.anything() }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); + }); + + it('should read int values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'int', 300); + expect(arg).toMatchObject({ name: 'a-test', type: 'int', value: 300 }); + }); + + it('should read VarInt values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'VarInt', 300); + expect(arg).toMatchObject({ name: 'a-test', type: 'VarInt', value: 300n }); + }); + + it('should read bool values', () => { + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool', true)) + .toMatchObject({ name: 'a-test', type: 'bool', value: true }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool', false)) + .toMatchObject({ name: 'a-test', type: 'bool', value: false }); + }); + + it('should read ContractId values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'ContractId', '74657374'); + expect(arg).toMatchObject({ name: 'a-test', type: 'ContractId', value: expect.anything() }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); + }); + + it('should read TokenUid values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'TokenUid', '74657374'); + expect(arg).toMatchObject({ name: 'a-test', type: 'TokenUid', value: expect.anything() }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); + }); + + it('should read Address values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address', 'test'); + expect(arg).toMatchObject({ name: 'a-test', type: 'Address', value: 'test' }); + }); + + // Optional + + it('should read Optional[int] values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[int]', 300); + expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[int]', value: 300 }); + + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[int]', null)) + .toMatchObject({ name: 'a-test', type: 'Optional[int]', value: null }); + }); + + it('should read Optional[VarInt] values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[VarInt]', 300); + expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[VarInt]', value: 300n }); + }); + + it('should read Optional[bool] values', () => { + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', true)) + .toMatchObject({ name: 'a-test', type: 'Optional[bool]', value: true }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', false)) + .toMatchObject({ name: 'a-test', type: 'Optional[bool]', value: false }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', null)) + .toMatchObject({ name: 'a-test', type: 'Optional[bool]', value: null }); + }); + + it('should read Optional[ContractId] values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[ContractId]', '74657374'); + expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[ContractId]', value: expect.anything() }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); + + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[ContractId]', null)) + .toMatchObject({ name: 'a-test', type: 'Optional[ContractId]', value: null }); + }); + + it('should read Optional[TokenUid] values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[TokenUid]', '74657374'); + expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[TokenUid]', value: expect.anything() }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); + + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[TokenUid]', null)) + .toMatchObject({ name: 'a-test', type: 'Optional[TokenUid]', value: null }); + }); + + it('should read Optional[Address] values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', 'test'); + expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[Address]', value: 'test' }); + + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', null)) + .toMatchObject({ name: 'a-test', type: 'Optional[Address]', value: null }); + }); + + it('should read int? values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'int?', 300); + expect(arg).toMatchObject({ name: 'a-test', type: 'int?', value: 300 }); + + expect(NanoContractMethodArgument.fromApiInput('a-test', 'int?', null)) + .toMatchObject({ name: 'a-test', type: 'int?', value: null }); + }); + + it('should read VarInt? values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'VarInt?', 300); + expect(arg).toMatchObject({ name: 'a-test', type: 'VarInt?', value: 300n }); + }); + + it('should read bool? values', () => { + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', true)) + .toMatchObject({ name: 'a-test', type: 'bool?', value: true }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', false)) + .toMatchObject({ name: 'a-test', type: 'bool?', value: false }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', null)) + .toMatchObject({ name: 'a-test', type: 'bool?', value: null }); + }); + + it('should read ContractId? values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'ContractId?', '74657374'); + expect(arg).toMatchObject({ name: 'a-test', type: 'ContractId?', value: expect.anything() }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); + + expect(NanoContractMethodArgument.fromApiInput('a-test', 'ContractId?', null)) + .toMatchObject({ name: 'a-test', type: 'ContractId?', value: null }); + }); + + it('should read TokenUid? values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'TokenUid?', '74657374'); + expect(arg).toMatchObject({ name: 'a-test', type: 'TokenUid?', value: expect.anything() }); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); + + expect(NanoContractMethodArgument.fromApiInput('a-test', 'TokenUid?', null)) + .toMatchObject({ name: 'a-test', type: 'TokenUid?', value: null }); + }); + + it('should read Address? values', () => { + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address?', 'test'); + expect(arg).toMatchObject({ name: 'a-test', type: 'Address?', value: 'test' }); + + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Address?', null)) + .toMatchObject({ name: 'a-test', type: 'Address?', value: null }); + }); }); diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index 7e3f3ce83..9a1ec5f64 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -13,10 +13,172 @@ import { NanoContractSignedData, BufferROExtract, NanoContractArgumentApiInputType, + NanoContractArgumentApiInputSchema, } from './types'; import Serializer from './serializer'; import Deserializer from './deserializer'; import { getContainerInternalType, getContainerType } from './utils'; +import { z } from 'zod'; + +/** + * Refinement method meant to validate, parse and return the transformed type. + * User input will be parsed, validated and converted to the actual internal TS type. + * Issues are added to the context so zod can show parse errors safely. + */ +function refineSingleValue(ctx: z.RefinementCtx, inputVal: NanoContractArgumentApiInputType, type: string) { + if (['int', 'Timestamp'].includes(type)) { + const parse = z.coerce.number().safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid ${type}: ${parse.error}`, + fatal: true, + }); + } else { + return parse.data; + } + } else if (type === 'VarInt') { + const parse = z.coerce.bigint().safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid VarInt: ${parse.error}`, + fatal: true, + }); + } else { + return parse.data; + } + } else if (['bytes', 'BlueprintId', 'ContractId', 'TokenUid', 'TxOutputScript', 'VertexId'].includes(type)) { + const parse = z.string().regex(/[0-9A-Fa-f]+/g).transform(val => Buffer.from(val, 'hex')).safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid ${type}: ${parse.error}`, + fatal: true, + }); + } else { + return parse.data; + } + } else if (type === 'bool') { + const parse = z + .boolean() + .or( + z.union([z.literal('true'), z.literal('false')]).transform(val => val === 'true') + ).safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid bool: ${parse.error}`, + fatal: true, + }); + } else { + return parse.data; + } + } else if (['str', 'Address'].includes(type)) { + const parse = z.string().safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid str: ${parse.error}`, + fatal: true, + }); + } else { + return parse.data; + } + } else { + // No known types match the given type + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Type(${type}) is not supported as a 'single' type`, + fatal: true, + }); + } + + // Meant to keep the typing correct + return z.NEVER; +} + +/** + * Type and value validation for non-container types. + * Returns the internal TS type for the argument given. + */ +const SingleValueApiInputScheme = z + .tuple([ + z.string(), // type + NanoContractArgumentApiInputSchema, // value + ]) + .transform((value, ctx) => { + return refineSingleValue(ctx, value[1], value[0]); + }) + +/** + * Type and value validation for Optional types. + * Returns the internal TS type for the argument given. + */ +const OptionalApiInputScheme = z + .tuple([ + z.string(), // Inner type + NanoContractArgumentApiInputSchema, // value + ]) + .transform((value, ctx) => { + const parse = z.null().safeParse(value[1]); + if (parse.success) { + return parse.data; + } else { + // value is not null, should transform based on the type + return refineSingleValue(ctx, value[1], value[0]); + } + }) + +/** + * Type and value validation for SignedData types. + * returns an instance of NanoContractSignedData + */ +const SignedDataApiInputScheme = z + .string() + .transform(value => (value.split(','))) + .pipe(z.tuple([ + z.string().regex(/[0-9A-Fa-f]+/g), + z.string().regex(/[0-9A-Fa-f]+/g), + z.string(), + z.string(), + ])) + .transform((value, ctx) => { + const signature = Buffer.from(value[0], 'hex'); + const ncID = Buffer.from(value[1], 'hex'); + const type = value[3]; + const refinedValue = refineSingleValue(ctx, value[2], type); + let ret: NanoContractSignedData = { + signature, + type, + value: [ncID, refinedValue], + }; + return ret; + }); + +/** + * Type and value validation for RawSignedData types. + * returns an instance of NanoContractRawSignedData + */ +const RawSignedDataApiInputScheme = z + .string() + .transform(value => (value.split(','))) + .pipe(z.tuple([ + z.string().regex(/[0-9A-Fa-f]+/g), + z.string(), + z.string(), + ])) + .transform((value, ctx) => { + const signature = Buffer.from(value[0], 'hex'); + const type = value[2]; + const refinedValue = refineSingleValue(ctx, value[1], type); + let ret: NanoContractRawSignedData = { + signature, + type, + value: refinedValue, + }; + return ret; + }); export class NanoContractMethodArgument { name: string; @@ -83,86 +245,38 @@ export class NanoContractMethodArgument { const [containerType, innerType] = getContainerInternalType(type); if (containerType === 'SignedData') { // Parse string SignedData into NanoContractSignedData - const splittedValue = (value as string).split(','); - if (splittedValue.length !== 4) { - throw new Error(); - } - const [signature, ncId, val, valType] = splittedValue; - if (valType.trim() !== innerType.trim()) { + const data = SignedDataApiInputScheme.parse(value); + if (data.type !== innerType.trim()) { throw new Error(); } - - let finalValue: NanoContractArgumentSingleType = val; - if (innerType === 'bytes') { - finalValue = Buffer.from(val, 'hex'); - } else if (innerType === 'bool') { - // If the result is expected as boolean, it will come here as a string true/false - finalValue = val === 'true'; - } else if (innerType === 'int') { - finalValue = Number.parseInt(val, 10); - } else if (innerType === 'VarInt') { - finalValue = BigInt(val); - } else { - // For the other types - finalValue = val; - } - - const data: NanoContractSignedData = { - type: innerType, - value: [Buffer.from(ncId, 'hex'), finalValue], - signature: Buffer.from(signature, 'hex'), - }; return new NanoContractMethodArgument(name, type, data); - } - if (containerType === 'RawSignedData') { + } else if (containerType === 'RawSignedData') { // Parse string RawSignedData into NanoContractRawSignedData - const splittedValue = (value as string).split(','); - if (splittedValue.length !== 3) { - throw new Error(); - } - const [signature, val, valType] = splittedValue; - if (valType.trim() !== innerType.trim()) { - throw new Error(); - } - - let finalValue: NanoContractArgumentSingleType = val; - if (innerType === 'bytes') { - finalValue = Buffer.from(val, 'hex'); - } else if (innerType === 'bool') { - // If the result is expected as boolean, it will come here as a string true/false - finalValue = val === 'true'; - } else if (innerType === 'int') { - finalValue = Number.parseInt(val, 10); - } else if (innerType === 'VarInt') { - finalValue = BigInt(val); - } else { - // For the other types - finalValue = val; - } - - const data: NanoContractRawSignedData = { - type: innerType, - value: finalValue, - signature: Buffer.from(signature, 'hex'), - }; + const data = RawSignedDataApiInputScheme.parse(value); + return new NanoContractMethodArgument(name, type, data); + } else if (containerType === 'Optional') { + const data = OptionalApiInputScheme.parse([innerType, value]); return new NanoContractMethodArgument(name, type, data); } - // XXX: Should we have a special case for Optional, Tuple? - } + // XXX: add special case for Tuple - return new NanoContractMethodArgument(name, type, value); + throw new Error(`ContainerType(${containerType}) is not supported as api input.`); + } + // This is a single value type and should + const data = SingleValueApiInputScheme.parse([type, value]); + return new NanoContractMethodArgument(name, type, data); } toApiInput(): NanoContractParsedArgument { function prepSingleValue(type: string, value: NanoContractArgumentSingleType) { - switch (type) { - case 'bytes': - return (value as Buffer).toString('hex'); - case 'bool': + if (type === 'bool') { return (value as boolean) ? 'true' : 'false'; - default: - return value; + } else if (['bytes', 'BlueprintId', 'ContractId', 'TokenUid', 'TxOutputScript', 'VertexId'].includes(type)) { + return (value as Buffer).toString('hex'); + } else if (type === 'VarInt') { + return String(value as bigint); } + return value; } if (this.type.startsWith('SignedData')) { @@ -195,7 +309,7 @@ export class NanoContractMethodArgument { return { name: this.name, type: this.type, - parsed: this.value, + parsed: prepSingleValue(this.type, (this.value as NanoContractArgumentSingleType)), }; } } diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 38cee4773..7102b29b0 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -75,14 +75,35 @@ export const NanoContractArgumentSchema = z.union([ ]); export type NanoContractArgumentType = z.output; +/** + * Single type names + */ +export const NanoContractArgumentSingleTypeNameSchema = z.enum([ + 'bool', + 'bytes', + 'int', + 'str', + 'Address', + 'BlueprintId', + 'ContractId', + 'Timestamp', + 'TokenUid', + 'TxOutputScript', + 'VarInt', + 'VertexId', +]); +export type NanoContractArgumentSingleTypeName = z.output; + /** * Container type names */ -export type NanoContractArgumentContainerType = - | 'Optional' - | 'SignedData' - | 'RawSignedData' - | 'Tuple'; +export const NanoContractArgumentContainerTypeNameSchema = z.enum([ + 'Optional', + 'SignedData', + 'RawSignedData', + 'Tuple', +]); +export type NanoContractArgumentContainerType = z.output; export enum NanoContractActionType { DEPOSIT = 'deposit', diff --git a/src/nano_contracts/utils.ts b/src/nano_contracts/utils.ts index 5c837b60f..9d28ea599 100644 --- a/src/nano_contracts/utils.ts +++ b/src/nano_contracts/utils.ts @@ -49,6 +49,7 @@ export function getContainerInternalType( case 'Tuple': case 'SignedData': case 'RawSignedData': + case 'Optional': return [containerType, internalType]; default: throw new Error('Not a ContainerType'); From f30b1fd9baa76e07f7d490dd365e8b982f87b8cc Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 16:07:51 -0300 Subject: [PATCH 09/23] chore:linter changes --- __tests__/nano_contracts/methodArg.test.ts | 118 +++++++++---- src/nano_contracts/methodArg.ts | 194 +++++++++++---------- src/nano_contracts/types.ts | 8 +- 3 files changed, 193 insertions(+), 127 deletions(-) diff --git a/__tests__/nano_contracts/methodArg.test.ts b/__tests__/nano_contracts/methodArg.test.ts index bc074e1e9..c30d79eae 100644 --- a/__tests__/nano_contracts/methodArg.test.ts +++ b/__tests__/nano_contracts/methodArg.test.ts @@ -191,10 +191,16 @@ describe('fromApiInput', () => { }); it('should read bool values', () => { - expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool', true)) - .toMatchObject({ name: 'a-test', type: 'bool', value: true }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool', false)) - .toMatchObject({ name: 'a-test', type: 'bool', value: false }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool', true)).toMatchObject({ + name: 'a-test', + type: 'bool', + value: true, + }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool', false)).toMatchObject({ + name: 'a-test', + type: 'bool', + value: false, + }); }); it('should read ContractId values', () => { @@ -222,8 +228,11 @@ describe('fromApiInput', () => { const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[int]', 300); expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[int]', value: 300 }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[int]', null)) - .toMatchObject({ name: 'a-test', type: 'Optional[int]', value: null }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[int]', null)).toMatchObject({ + name: 'a-test', + type: 'Optional[int]', + value: null, + }); }); it('should read Optional[VarInt] values', () => { @@ -232,48 +241,69 @@ describe('fromApiInput', () => { }); it('should read Optional[bool] values', () => { - expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', true)) - .toMatchObject({ name: 'a-test', type: 'Optional[bool]', value: true }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', false)) - .toMatchObject({ name: 'a-test', type: 'Optional[bool]', value: false }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', null)) - .toMatchObject({ name: 'a-test', type: 'Optional[bool]', value: null }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', true)).toMatchObject( + { name: 'a-test', type: 'Optional[bool]', value: true } + ); + expect( + NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', false) + ).toMatchObject({ name: 'a-test', type: 'Optional[bool]', value: false }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[bool]', null)).toMatchObject( + { name: 'a-test', type: 'Optional[bool]', value: null } + ); }); it('should read Optional[ContractId] values', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[ContractId]', '74657374'); - expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[ContractId]', value: expect.anything() }); + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'Optional[ContractId]', + '74657374' + ); + expect(arg).toMatchObject({ + name: 'a-test', + type: 'Optional[ContractId]', + value: expect.anything(), + }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[ContractId]', null)) - .toMatchObject({ name: 'a-test', type: 'Optional[ContractId]', value: null }); + expect( + NanoContractMethodArgument.fromApiInput('a-test', 'Optional[ContractId]', null) + ).toMatchObject({ name: 'a-test', type: 'Optional[ContractId]', value: null }); }); it('should read Optional[TokenUid] values', () => { const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[TokenUid]', '74657374'); - expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[TokenUid]', value: expect.anything() }); + expect(arg).toMatchObject({ + name: 'a-test', + type: 'Optional[TokenUid]', + value: expect.anything(), + }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[TokenUid]', null)) - .toMatchObject({ name: 'a-test', type: 'Optional[TokenUid]', value: null }); + expect( + NanoContractMethodArgument.fromApiInput('a-test', 'Optional[TokenUid]', null) + ).toMatchObject({ name: 'a-test', type: 'Optional[TokenUid]', value: null }); }); it('should read Optional[Address] values', () => { const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', 'test'); expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[Address]', value: 'test' }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', null)) - .toMatchObject({ name: 'a-test', type: 'Optional[Address]', value: null }); + expect( + NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', null) + ).toMatchObject({ name: 'a-test', type: 'Optional[Address]', value: null }); }); it('should read int? values', () => { const arg = NanoContractMethodArgument.fromApiInput('a-test', 'int?', 300); expect(arg).toMatchObject({ name: 'a-test', type: 'int?', value: 300 }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'int?', null)) - .toMatchObject({ name: 'a-test', type: 'int?', value: null }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'int?', null)).toMatchObject({ + name: 'a-test', + type: 'int?', + value: null, + }); }); it('should read VarInt? values', () => { @@ -282,12 +312,21 @@ describe('fromApiInput', () => { }); it('should read bool? values', () => { - expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', true)) - .toMatchObject({ name: 'a-test', type: 'bool?', value: true }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', false)) - .toMatchObject({ name: 'a-test', type: 'bool?', value: false }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', null)) - .toMatchObject({ name: 'a-test', type: 'bool?', value: null }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', true)).toMatchObject({ + name: 'a-test', + type: 'bool?', + value: true, + }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', false)).toMatchObject({ + name: 'a-test', + type: 'bool?', + value: false, + }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'bool?', null)).toMatchObject({ + name: 'a-test', + type: 'bool?', + value: null, + }); }); it('should read ContractId? values', () => { @@ -296,8 +335,11 @@ describe('fromApiInput', () => { // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'ContractId?', null)) - .toMatchObject({ name: 'a-test', type: 'ContractId?', value: null }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'ContractId?', null)).toMatchObject({ + name: 'a-test', + type: 'ContractId?', + value: null, + }); }); it('should read TokenUid? values', () => { @@ -306,15 +348,21 @@ describe('fromApiInput', () => { // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect(arg.value).toMatchBuffer(Buffer.from('74657374', 'hex')); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'TokenUid?', null)) - .toMatchObject({ name: 'a-test', type: 'TokenUid?', value: null }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'TokenUid?', null)).toMatchObject({ + name: 'a-test', + type: 'TokenUid?', + value: null, + }); }); it('should read Address? values', () => { const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address?', 'test'); expect(arg).toMatchObject({ name: 'a-test', type: 'Address?', value: 'test' }); - expect(NanoContractMethodArgument.fromApiInput('a-test', 'Address?', null)) - .toMatchObject({ name: 'a-test', type: 'Address?', value: null }); + expect(NanoContractMethodArgument.fromApiInput('a-test', 'Address?', null)).toMatchObject({ + name: 'a-test', + type: 'Address?', + value: null, + }); }); }); diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index 9a1ec5f64..97b3c3e99 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import { z } from 'zod'; import { NanoContractArgumentSingleType, NanoContractArgumentType, @@ -18,81 +19,89 @@ import { import Serializer from './serializer'; import Deserializer from './deserializer'; import { getContainerInternalType, getContainerType } from './utils'; -import { z } from 'zod'; /** * Refinement method meant to validate, parse and return the transformed type. * User input will be parsed, validated and converted to the actual internal TS type. * Issues are added to the context so zod can show parse errors safely. */ -function refineSingleValue(ctx: z.RefinementCtx, inputVal: NanoContractArgumentApiInputType, type: string) { - if (['int', 'Timestamp'].includes(type)) { - const parse = z.coerce.number().safeParse(inputVal); - if (!parse.success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Value is invalid ${type}: ${parse.error}`, - fatal: true, - }); - } else { - return parse.data; - } - } else if (type === 'VarInt') { - const parse = z.coerce.bigint().safeParse(inputVal); - if (!parse.success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Value is invalid VarInt: ${parse.error}`, - fatal: true, - }); - } else { - return parse.data; - } - } else if (['bytes', 'BlueprintId', 'ContractId', 'TokenUid', 'TxOutputScript', 'VertexId'].includes(type)) { - const parse = z.string().regex(/[0-9A-Fa-f]+/g).transform(val => Buffer.from(val, 'hex')).safeParse(inputVal); - if (!parse.success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Value is invalid ${type}: ${parse.error}`, - fatal: true, - }); - } else { - return parse.data; - } - } else if (type === 'bool') { - const parse = z - .boolean() - .or( - z.union([z.literal('true'), z.literal('false')]).transform(val => val === 'true') - ).safeParse(inputVal); - if (!parse.success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Value is invalid bool: ${parse.error}`, - fatal: true, - }); - } else { - return parse.data; - } - } else if (['str', 'Address'].includes(type)) { - const parse = z.string().safeParse(inputVal); - if (!parse.success) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Value is invalid str: ${parse.error}`, - fatal: true, - }); - } else { - return parse.data; - } +function refineSingleValue( + ctx: z.RefinementCtx, + inputVal: NanoContractArgumentApiInputType, + type: string +) { + if (['int', 'Timestamp'].includes(type)) { + const parse = z.coerce.number().safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid ${type}: ${parse.error}`, + fatal: true, + }); } else { - // No known types match the given type + return parse.data; + } + } else if (type === 'VarInt') { + const parse = z.coerce.bigint().safeParse(inputVal); + if (!parse.success) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Type(${type}) is not supported as a 'single' type`, + message: `Value is invalid VarInt: ${parse.error}`, fatal: true, }); + } else { + return parse.data; } + } else if ( + ['bytes', 'BlueprintId', 'ContractId', 'TokenUid', 'TxOutputScript', 'VertexId'].includes(type) + ) { + const parse = z + .string() + .regex(/[0-9A-Fa-f]+/g) + .transform(val => Buffer.from(val, 'hex')) + .safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid ${type}: ${parse.error}`, + fatal: true, + }); + } else { + return parse.data; + } + } else if (type === 'bool') { + const parse = z + .boolean() + .or(z.union([z.literal('true'), z.literal('false')]).transform(val => val === 'true')) + .safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid bool: ${parse.error}`, + fatal: true, + }); + } else { + return parse.data; + } + } else if (['str', 'Address'].includes(type)) { + const parse = z.string().safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid str: ${parse.error}`, + fatal: true, + }); + } else { + return parse.data; + } + } else { + // No known types match the given type + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Type(${type}) is not supported as a 'single' type`, + fatal: true, + }); + } // Meant to keep the typing correct return z.NEVER; @@ -109,7 +118,7 @@ const SingleValueApiInputScheme = z ]) .transform((value, ctx) => { return refineSingleValue(ctx, value[1], value[0]); - }) + }); /** * Type and value validation for Optional types. @@ -124,11 +133,10 @@ const OptionalApiInputScheme = z const parse = z.null().safeParse(value[1]); if (parse.success) { return parse.data; - } else { - // value is not null, should transform based on the type - return refineSingleValue(ctx, value[1], value[0]); } - }) + // value is not null, should transform based on the type + return refineSingleValue(ctx, value[1], value[0]); + }); /** * Type and value validation for SignedData types. @@ -136,19 +144,21 @@ const OptionalApiInputScheme = z */ const SignedDataApiInputScheme = z .string() - .transform(value => (value.split(','))) - .pipe(z.tuple([ - z.string().regex(/[0-9A-Fa-f]+/g), - z.string().regex(/[0-9A-Fa-f]+/g), - z.string(), - z.string(), - ])) + .transform(value => value.split(',')) + .pipe( + z.tuple([ + z.string().regex(/[0-9A-Fa-f]+/g), + z.string().regex(/[0-9A-Fa-f]+/g), + z.string(), + z.string(), + ]) + ) .transform((value, ctx) => { const signature = Buffer.from(value[0], 'hex'); const ncID = Buffer.from(value[1], 'hex'); const type = value[3]; const refinedValue = refineSingleValue(ctx, value[2], type); - let ret: NanoContractSignedData = { + const ret: NanoContractSignedData = { signature, type, value: [ncID, refinedValue], @@ -162,17 +172,13 @@ const SignedDataApiInputScheme = z */ const RawSignedDataApiInputScheme = z .string() - .transform(value => (value.split(','))) - .pipe(z.tuple([ - z.string().regex(/[0-9A-Fa-f]+/g), - z.string(), - z.string(), - ])) + .transform(value => value.split(',')) + .pipe(z.tuple([z.string().regex(/[0-9A-Fa-f]+/g), z.string(), z.string()])) .transform((value, ctx) => { const signature = Buffer.from(value[0], 'hex'); const type = value[2]; const refinedValue = refineSingleValue(ctx, value[1], type); - let ret: NanoContractRawSignedData = { + const ret: NanoContractRawSignedData = { signature, type, value: refinedValue, @@ -250,11 +256,13 @@ export class NanoContractMethodArgument { throw new Error(); } return new NanoContractMethodArgument(name, type, data); - } else if (containerType === 'RawSignedData') { + } + if (containerType === 'RawSignedData') { // Parse string RawSignedData into NanoContractRawSignedData const data = RawSignedDataApiInputScheme.parse(value); return new NanoContractMethodArgument(name, type, data); - } else if (containerType === 'Optional') { + } + if (containerType === 'Optional') { const data = OptionalApiInputScheme.parse([innerType, value]); return new NanoContractMethodArgument(name, type, data); } @@ -270,10 +278,16 @@ export class NanoContractMethodArgument { toApiInput(): NanoContractParsedArgument { function prepSingleValue(type: string, value: NanoContractArgumentSingleType) { if (type === 'bool') { - return (value as boolean) ? 'true' : 'false'; - } else if (['bytes', 'BlueprintId', 'ContractId', 'TokenUid', 'TxOutputScript', 'VertexId'].includes(type)) { - return (value as Buffer).toString('hex'); - } else if (type === 'VarInt') { + return (value as boolean) ? 'true' : 'false'; + } + if ( + ['bytes', 'BlueprintId', 'ContractId', 'TokenUid', 'TxOutputScript', 'VertexId'].includes( + type + ) + ) { + return (value as Buffer).toString('hex'); + } + if (type === 'VarInt') { return String(value as bigint); } return value; @@ -309,7 +323,7 @@ export class NanoContractMethodArgument { return { name: this.name, type: this.type, - parsed: prepSingleValue(this.type, (this.value as NanoContractArgumentSingleType)), + parsed: prepSingleValue(this.type, this.value as NanoContractArgumentSingleType), }; } } diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 7102b29b0..92528ad06 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -92,7 +92,9 @@ export const NanoContractArgumentSingleTypeNameSchema = z.enum([ 'VarInt', 'VertexId', ]); -export type NanoContractArgumentSingleTypeName = z.output; +export type NanoContractArgumentSingleTypeName = z.output< + typeof NanoContractArgumentSingleTypeNameSchema +>; /** * Container type names @@ -103,7 +105,9 @@ export const NanoContractArgumentContainerTypeNameSchema = z.enum([ 'RawSignedData', 'Tuple', ]); -export type NanoContractArgumentContainerType = z.output; +export type NanoContractArgumentContainerType = z.output< + typeof NanoContractArgumentContainerTypeNameSchema +>; export enum NanoContractActionType { DEPOSIT = 'deposit', From 91f551c2166e01a03fddd215f16a1d225d819a97 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 16:28:33 -0300 Subject: [PATCH 10/23] chore: add branch to list for CI --- .github/workflows/integration-test.yml | 2 ++ .github/workflows/lint.yml | 2 ++ .github/workflows/unit-test.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 373d37a45..547aa6f65 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -6,6 +6,7 @@ on: - release - release-candidate - feat/nc-args-serialization + - feat/nc-args-class tags: - v* pull_request: @@ -14,6 +15,7 @@ on: - release-candidate - master - feat/nc-args-serialization + - feat/nc-args-class env: TEST_WALLET_START_TIMEOUT: '180000' # 3 minutes diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f5802b0bf..29fa628bf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,6 +6,7 @@ on: - release - release-candidate - feat/nc-args-serialization + - feat/nc-args-class tags: - v* pull_request: @@ -14,6 +15,7 @@ on: - release-candidate - master - feat/nc-args-serialization + - feat/nc-args-class jobs: linter: runs-on: 'ubuntu-latest' diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 627ad75c7..cef3095e0 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -6,6 +6,7 @@ on: - release - release-candidate - feat/nc-args-serialization + - feat/nc-args-class tags: - v* pull_request: @@ -14,6 +15,7 @@ on: - release-candidate - master - feat/nc-args-serialization + - feat/nc-args-class jobs: test: runs-on: 'ubuntu-latest' From 1fb8b27f91c39e3c53eb747dbe62b315a91d88ef Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 18:43:36 -0300 Subject: [PATCH 11/23] feat: address validation from zod --- .../integration/nanocontracts/bet.test.ts | 2 +- src/models/address.ts | 16 +-- src/nano_contracts/builder.ts | 43 ++----- src/nano_contracts/methodArg.ts | 24 +++- src/nano_contracts/utils.ts | 105 +++--------------- 5 files changed, 61 insertions(+), 129 deletions(-) diff --git a/__tests__/integration/nanocontracts/bet.test.ts b/__tests__/integration/nanocontracts/bet.test.ts index 8763f9656..422df28ce 100644 --- a/__tests__/integration/nanocontracts/bet.test.ts +++ b/__tests__/integration/nanocontracts/bet.test.ts @@ -327,7 +327,7 @@ describe('full cycle of bet nano contract', () => { const inputData = await getOracleInputData(oracleData, resultSerialized, wallet); const txSetResult = await wallet.createAndSendNanoContractTransaction('set_result', address1, { ncId: tx1.hash, - args: [`${bufferToHex(inputData)},${result},str`], + args: [`${bufferToHex(inputData)},${bufferToHex(tx1.hash)},${result},str`], }); await checkTxValid(wallet, txSetResult); txIds.push(txSetResult.hash); diff --git a/src/models/address.ts b/src/models/address.ts index 5caff3a7f..56ae86859 100644 --- a/src/models/address.ts +++ b/src/models/address.ts @@ -77,7 +77,7 @@ class Address { * @memberof Address * @inner */ - validateAddress(): boolean { + validateAddress({ skipNetwork }: { skipNetwork: boolean } = { skipNetwork: false }): boolean { const addressBytes = this.decode(); const errorMessage = `Invalid address: ${this.base58}.`; @@ -98,12 +98,14 @@ class Address { ); } - // Validate version byte. Should be the p2pkh or p2sh - const firstByte = addressBytes[0]; - if (!this.network.isVersionByteValid(firstByte)) { - throw new AddressError( - `${errorMessage} Invalid network byte. Expected: ${this.network.versionBytes.p2pkh} or ${this.network.versionBytes.p2sh} and received ${firstByte}.` - ); + if (skipNetwork) { + // Validate version byte. Should be the p2pkh or p2sh + const firstByte = addressBytes[0]; + if (!this.network.isVersionByteValid(firstByte)) { + throw new AddressError( + `${errorMessage} Invalid network byte. Expected: ${this.network.versionBytes.p2pkh} or ${this.network.versionBytes.p2sh} and received ${firstByte}.` + ); + } } return true; } diff --git a/src/nano_contracts/builder.ts b/src/nano_contracts/builder.ts index c9f01ad85..a0594169f 100644 --- a/src/nano_contracts/builder.ts +++ b/src/nano_contracts/builder.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { concat, get } from 'lodash'; +import { concat } from 'lodash'; import Output from '../models/output'; import Input from '../models/input'; import Transaction from '../models/transaction'; @@ -20,15 +20,12 @@ import { NanoContractActionHeader, NanoContractActionType, NanoContractAction, - MethodArgInfo, NanoContractArgumentApiInputType, } from './types'; import { ITokenData } from '../types'; -import ncApi from '../api/nano'; -import { validateAndUpdateBlueprintMethodArgs } from './utils'; +import { validateAndParseBlueprintMethodArgs } from './utils'; import NanoContractHeader from './header'; import leb128 from '../utils/leb128'; -import { NanoContractMethodArgument } from './methodArg'; class NanoContractTransactionBuilder { blueprintId: string | null | undefined; @@ -302,7 +299,11 @@ class NanoContractTransactionBuilder { } // Validate if the arguments match the expected method arguments - await validateAndUpdateBlueprintMethodArgs(this.blueprintId, this.method, this.args); + const parsedArgs = await validateAndParseBlueprintMethodArgs( + this.blueprintId, + this.method, + this.args + ); // Transform actions into inputs and outputs let inputs: Input[] = []; @@ -333,33 +334,11 @@ class NanoContractTransactionBuilder { } // Serialize the method arguments - const serializedArgs: Buffer[] = [leb128.encodeUnsigned(this.args?.length ?? 0)]; - if (this.args) { + const serializedArgs: Buffer[] = [leb128.encodeUnsigned(parsedArgs?.length ?? 0)]; + if (parsedArgs) { const serializer = new Serializer(this.wallet.getNetworkObject()); - const blueprintInformation = await ncApi.getBlueprintInformation(this.blueprintId); - const methodArgs = get( - blueprintInformation, - `public_methods.${this.method}.args`, - [] - ) as MethodArgInfo[]; - if (!methodArgs) { - throw new NanoContractTransactionError(`Blueprint does not have method ${this.method}.`); - } - - if (this.args.length !== methodArgs.length) { - throw new NanoContractTransactionError( - `Method needs ${methodArgs.length} parameters but data has ${this.args.length}.` - ); - } - - for (const [index, arg] of methodArgs.entries()) { - const methodArg = NanoContractMethodArgument.fromApiInput( - arg.name, - arg.type, - this.args[index] - ); - const serialized = methodArg.serialize(serializer); - serializedArgs.push(serialized); + for (const arg of parsedArgs) { + serializedArgs.push(arg.serialize(serializer)); } } diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index 97b3c3e99..33e8b5ccb 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -19,6 +19,7 @@ import { import Serializer from './serializer'; import Deserializer from './deserializer'; import { getContainerInternalType, getContainerType } from './utils'; +import Address from '../models/address'; /** * Refinement method meant to validate, parse and return the transformed type. @@ -83,7 +84,7 @@ function refineSingleValue( } else { return parse.data; } - } else if (['str', 'Address'].includes(type)) { + } else if (type === 'str') { const parse = z.string().safeParse(inputVal); if (!parse.success) { ctx.addIssue({ @@ -94,6 +95,27 @@ function refineSingleValue( } else { return parse.data; } + } else if (type === 'Address') { + const parse = z.string().safeParse(inputVal); + if (!parse.success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid Address: ${parse.error}`, + fatal: true, + }); + } else { + const address = new Address(parse.data); + try { + address.validateAddress({ skipNetwork: true }); + return parse.data; + } catch (err) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Value is invalid Address: ${err instanceof Error ? err.message : String(err)}`, + fatal: true, + }); + } + } } else { // No known types match the given type ctx.addIssue({ diff --git a/src/nano_contracts/utils.ts b/src/nano_contracts/utils.ts index 9d28ea599..bd7c13855 100644 --- a/src/nano_contracts/utils.ts +++ b/src/nano_contracts/utils.ts @@ -7,6 +7,7 @@ import { get } from 'lodash'; import { crypto } from 'bitcore-lib'; +import { z } from 'zod'; import transactionUtils from '../utils/transaction'; import SendTransaction from '../new/sendTransaction'; import HathorWallet from '../new/wallet'; @@ -24,10 +25,11 @@ import { IHistoryTx, IStorage } from '../types'; import { parseScript } from '../utils/scripts'; import { MethodArgInfo, - NanoContractArgumentType, NanoContractArgumentContainerType, + NanoContractArgumentApiInputType, } from './types'; import { NANO_CONTRACTS_INITIALIZE_METHOD } from '../constants'; +import { NanoContractMethodArgument } from './methodArg'; export function getContainerInternalType( type: string @@ -169,16 +171,13 @@ export const getOracleInputData = async ( * @param method Method name * @param args Arguments of the method to check if have the expected types * - * Warning: This method can mutate the `args` parameter during its validation - * - * @throws NanoContractTransactionError in case the arguments are not valid * @throws NanoRequest404Error in case the blueprint ID does not exist on the full node */ -export const validateAndUpdateBlueprintMethodArgs = async ( +export const validateAndParseBlueprintMethodArgs = async ( blueprintId: string, method: string, - args: NanoContractArgumentType[] | null -): Promise => { + args: NanoContractArgumentApiInputType[] | null +): Promise => { // Get the blueprint data from full node const blueprintInformation = await ncApi.getBlueprintInformation(blueprintId); @@ -199,7 +198,7 @@ export const validateAndUpdateBlueprintMethodArgs = async ( ); } - return; + return null; } const argsLen = args.length; @@ -209,88 +208,18 @@ export const validateAndUpdateBlueprintMethodArgs = async ( ); } - // Here we validate that the arguments sent in the data array of args has - // the expected type for each parameter of the blueprint method - // Besides that, there are arguments that come from the clients in a different way - // that we expect, e.g. the bytes arguments come as hexadecimal, and the address - // arguments come as base58 strings, so we converts them and update the original - // array of arguments with the expected type - for (const [index, arg] of methodArgs.entries()) { - let typeToCheck = arg.type; - if (typeToCheck.startsWith('SignedData')) { - // Signed data will always be an hexadecimal with the - // signature len, signature, and the data itself - typeToCheck = 'str'; + try { + const parsedArgs: NanoContractMethodArgument[] = []; + for (const [index, arg] of methodArgs.entries()) { + const parsedArg = NanoContractMethodArgument.fromApiInput(arg.name, arg.type, args[index]); + parsedArgs.push(parsedArg); } - switch (typeToCheck) { - case 'bytes': - case 'BlueprintId': - case 'ContractId': - case 'TokenUid': - case 'TxOutputScript': - case 'VertexId': - // Bytes arguments are sent in hexadecimal - try { - // eslint-disable-next-line no-param-reassign - args[index] = hexToBuffer(args[index] as string); - } catch { - // Data sent is not a hex - throw new NanoContractTransactionError( - `Invalid hexadecimal for argument number ${index + 1} for type ${arg.type}.` - ); - } - break; - case 'Amount': - if (typeof args[index] !== 'bigint') { - throw new NanoContractTransactionError( - `Expects argument number ${index + 1} type ${arg.type} (bigint) but received type ${typeof args[index]}.` - ); - } - break; - case 'int': - case 'Timestamp': - if (typeof args[index] !== 'number') { - throw new NanoContractTransactionError( - `Expects argument number ${index + 1} type ${arg.type} but received type ${typeof args[index]}.` - ); - } - break; - case 'str': - if (typeof args[index] !== 'string') { - throw new NanoContractTransactionError( - `Expects argument number ${index + 1} type ${arg.type} but received type ${typeof args[index]}.` - ); - } - break; - // Creating a block {} in the case below - // because we can't create a variable without it (linter - no-case-declarations) - case 'Address': { - const argValue = args[index]; - if (typeof argValue !== 'string') { - throw new NanoContractTransactionError( - `Expects argument number ${index + 1} type ${arg.type} but received type ${typeof argValue}.` - ); - } - - try { - const address = new Address(argValue as string); - address.validateAddress(); - } catch { - // Argument value is not a valid address - throw new NanoContractTransactionError( - `Argument ${argValue} is not a valid base58 address.` - ); - } - break; - } - default: - // eslint-disable-next-line valid-typeof -- This rule is not suited for dynamic comparisons such as this one - if (arg.type !== typeof args[index]) { - throw new NanoContractTransactionError( - `Expects argument number ${index + 1} type ${arg.type} but received type ${typeof args[index]}.` - ); - } + return parsedArgs; + } catch (err: unknown) { + if (err instanceof z.ZodError || err instanceof Error) { + throw new NanoContractTransactionError(err.message); } + throw err; } }; From 9a29d841a3cc5f86496b2b713779961e009cadc4 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 18:59:45 -0300 Subject: [PATCH 12/23] test: update test values for Address --- __tests__/nano_contracts/methodArg.test.ts | 12 ++++++------ src/models/address.ts | 16 +++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/__tests__/nano_contracts/methodArg.test.ts b/__tests__/nano_contracts/methodArg.test.ts index c30d79eae..3dc48c8f2 100644 --- a/__tests__/nano_contracts/methodArg.test.ts +++ b/__tests__/nano_contracts/methodArg.test.ts @@ -218,8 +218,8 @@ describe('fromApiInput', () => { }); it('should read Address values', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address', 'test'); - expect(arg).toMatchObject({ name: 'a-test', type: 'Address', value: 'test' }); + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address', 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo'); + expect(arg).toMatchObject({ name: 'a-test', type: 'Address', value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' }); }); // Optional @@ -287,8 +287,8 @@ describe('fromApiInput', () => { }); it('should read Optional[Address] values', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', 'test'); - expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[Address]', value: 'test' }); + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo'); + expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[Address]', value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' }); expect( NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', null) @@ -356,8 +356,8 @@ describe('fromApiInput', () => { }); it('should read Address? values', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address?', 'test'); - expect(arg).toMatchObject({ name: 'a-test', type: 'Address?', value: 'test' }); + const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address?', 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo'); + expect(arg).toMatchObject({ name: 'a-test', type: 'Address?', value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' }); expect(NanoContractMethodArgument.fromApiInput('a-test', 'Address?', null)).toMatchObject({ name: 'a-test', diff --git a/src/models/address.ts b/src/models/address.ts index 56ae86859..98dc2f0f2 100644 --- a/src/models/address.ts +++ b/src/models/address.ts @@ -99,13 +99,15 @@ class Address { } if (skipNetwork) { - // Validate version byte. Should be the p2pkh or p2sh - const firstByte = addressBytes[0]; - if (!this.network.isVersionByteValid(firstByte)) { - throw new AddressError( - `${errorMessage} Invalid network byte. Expected: ${this.network.versionBytes.p2pkh} or ${this.network.versionBytes.p2sh} and received ${firstByte}.` - ); - } + return true + } + + // Validate version byte. Should be the p2pkh or p2sh + const firstByte = addressBytes[0]; + if (!this.network.isVersionByteValid(firstByte)) { + throw new AddressError( + `${errorMessage} Invalid network byte. Expected: ${this.network.versionBytes.p2pkh} or ${this.network.versionBytes.p2sh} and received ${firstByte}.` + ); } return true; } From fd1a81d853eb1f5ed1ca717f27b37aeb5f9c8eb0 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 19:03:08 -0300 Subject: [PATCH 13/23] chore: linter changes --- __tests__/nano_contracts/methodArg.test.ts | 36 ++++++++++++++++++---- src/models/address.ts | 2 +- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/__tests__/nano_contracts/methodArg.test.ts b/__tests__/nano_contracts/methodArg.test.ts index 3dc48c8f2..56c197293 100644 --- a/__tests__/nano_contracts/methodArg.test.ts +++ b/__tests__/nano_contracts/methodArg.test.ts @@ -218,8 +218,16 @@ describe('fromApiInput', () => { }); it('should read Address values', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address', 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo'); - expect(arg).toMatchObject({ name: 'a-test', type: 'Address', value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' }); + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'Address', + 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' + ); + expect(arg).toMatchObject({ + name: 'a-test', + type: 'Address', + value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo', + }); }); // Optional @@ -287,8 +295,16 @@ describe('fromApiInput', () => { }); it('should read Optional[Address] values', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo'); - expect(arg).toMatchObject({ name: 'a-test', type: 'Optional[Address]', value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' }); + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'Optional[Address]', + 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' + ); + expect(arg).toMatchObject({ + name: 'a-test', + type: 'Optional[Address]', + value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo', + }); expect( NanoContractMethodArgument.fromApiInput('a-test', 'Optional[Address]', null) @@ -356,8 +372,16 @@ describe('fromApiInput', () => { }); it('should read Address? values', () => { - const arg = NanoContractMethodArgument.fromApiInput('a-test', 'Address?', 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo'); - expect(arg).toMatchObject({ name: 'a-test', type: 'Address?', value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' }); + const arg = NanoContractMethodArgument.fromApiInput( + 'a-test', + 'Address?', + 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo' + ); + expect(arg).toMatchObject({ + name: 'a-test', + type: 'Address?', + value: 'WZ7pDnkPnxbs14GHdUFivFzPbzitwNtvZo', + }); expect(NanoContractMethodArgument.fromApiInput('a-test', 'Address?', null)).toMatchObject({ name: 'a-test', diff --git a/src/models/address.ts b/src/models/address.ts index 98dc2f0f2..f290a7f9a 100644 --- a/src/models/address.ts +++ b/src/models/address.ts @@ -99,7 +99,7 @@ class Address { } if (skipNetwork) { - return true + return true; } // Validate version byte. Should be the p2pkh or p2sh From ba68fddef132d78c65e080a4883814e8128d14db Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Tue, 13 May 2025 19:55:05 -0300 Subject: [PATCH 14/23] fix: hex string regex --- src/nano_contracts/methodArg.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index 33e8b5ccb..0871046ff 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -58,7 +58,7 @@ function refineSingleValue( ) { const parse = z .string() - .regex(/[0-9A-Fa-f]+/g) + .regex(/^[0-9A-Fa-f]+$/) .transform(val => Buffer.from(val, 'hex')) .safeParse(inputVal); if (!parse.success) { @@ -169,8 +169,8 @@ const SignedDataApiInputScheme = z .transform(value => value.split(',')) .pipe( z.tuple([ - z.string().regex(/[0-9A-Fa-f]+/g), - z.string().regex(/[0-9A-Fa-f]+/g), + z.string().regex(/^[0-9A-Fa-f]+$/), + z.string().regex(/^[0-9A-Fa-f]+$/), z.string(), z.string(), ]) @@ -195,7 +195,7 @@ const SignedDataApiInputScheme = z const RawSignedDataApiInputScheme = z .string() .transform(value => value.split(',')) - .pipe(z.tuple([z.string().regex(/[0-9A-Fa-f]+/g), z.string(), z.string()])) + .pipe(z.tuple([z.string().regex(/^[0-9A-Fa-f]+$/), z.string(), z.string()])) .transform((value, ctx) => { const signature = Buffer.from(value[0], 'hex'); const type = value[2]; From dc55248d59957bcb4b5c0ffe50cee736b7d12da0 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Wed, 14 May 2025 13:13:13 -0300 Subject: [PATCH 15/23] chore: wip --- .../integration/nanocontracts/bet.test.ts | 6 +-- __tests__/nano_contracts/deserializer.test.ts | 50 +++++++++++-------- __tests__/nano_contracts/methodArg.test.ts | 24 ++++----- src/nano_contracts/deserializer.ts | 3 +- src/nano_contracts/methodArg.ts | 9 ++-- src/nano_contracts/serializer.ts | 22 ++++++-- src/nano_contracts/types.ts | 3 +- src/nano_contracts/utils.ts | 15 ++++++ 8 files changed, 85 insertions(+), 47 deletions(-) diff --git a/__tests__/integration/nanocontracts/bet.test.ts b/__tests__/integration/nanocontracts/bet.test.ts index 422df28ce..d1805f4ba 100644 --- a/__tests__/integration/nanocontracts/bet.test.ts +++ b/__tests__/integration/nanocontracts/bet.test.ts @@ -324,7 +324,7 @@ describe('full cycle of bet nano contract', () => { const nanoSerializer = new Serializer(network); const result = '1x0'; const resultSerialized = nanoSerializer.serializeFromType(result, 'str'); - const inputData = await getOracleInputData(oracleData, resultSerialized, wallet); + const inputData = await getOracleInputData(oracleData, Buffer.from(tx1.hash, 'hex'), resultSerialized, wallet); const txSetResult = await wallet.createAndSendNanoContractTransaction('set_result', address1, { ncId: tx1.hash, args: [`${bufferToHex(inputData)},${bufferToHex(tx1.hash)},${result},str`], @@ -370,10 +370,10 @@ describe('full cycle of bet nano contract', () => { // @ts-expect-error toMatchBuffer is defined in setupTests.js ).toMatchBuffer(inputData); expect( - (txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value[0] + (txSetResultParser.parsedArgs[0].value as NanoContractSignedData).ncId // @ts-expect-error toMatchBuffer is defined in setupTests.js ).toMatchBuffer(Buffer.from(tx1.hash, 'hex')); - expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value[1]).toEqual( + expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value).toEqual( result ); diff --git a/__tests__/nano_contracts/deserializer.test.ts b/__tests__/nano_contracts/deserializer.test.ts index 806504f6f..e85266741 100644 --- a/__tests__/nano_contracts/deserializer.test.ts +++ b/__tests__/nano_contracts/deserializer.test.ts @@ -188,7 +188,8 @@ test('SignedData', () => { const valueInt: NanoContractSignedData = { type: 'int', - value: [Buffer.from('6e634944', 'hex'), 300], + value: 300, + ncId: Buffer.from('6e634944', 'hex'), signature: Buffer.from('74657374', 'hex'), }; const serializedInt = serializer.serializeFromType(valueInt, 'SignedData[int]'); @@ -201,12 +202,13 @@ test('SignedData', () => { // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((deserializedInt as NanoContractSignedData).signature).toMatchBuffer(valueInt.signature); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedInt as NanoContractSignedData).value[0]).toMatchBuffer(valueInt.value[0]); - expect((deserializedInt as NanoContractSignedData).value[1]).toEqual(valueInt.value[1]); + expect((deserializedInt as NanoContractSignedData).ncId).toMatchBuffer(valueInt.ncId); + expect((deserializedInt as NanoContractSignedData).value).toEqual(valueInt.value); const valueStr: NanoContractSignedData = { type: 'str', - value: [Buffer.from('6e634944', 'hex'), 'test'], + ncId: Buffer.from('6e634944', 'hex'), + value: 'test', signature: Buffer.from('74657374', 'hex'), }; const serializedStr = serializer.serializeFromType(valueStr, 'SignedData[str]'); @@ -219,12 +221,13 @@ test('SignedData', () => { // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((deserializedStr as NanoContractSignedData).signature).toMatchBuffer(valueStr.signature); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedStr as NanoContractSignedData).value[0]).toMatchBuffer(valueStr.value[0]); - expect((deserializedStr as NanoContractSignedData).value[1]).toEqual(valueStr.value[1]); + expect((deserializedStr as NanoContractSignedData).ncId).toMatchBuffer(valueStr.ncId); + expect((deserializedStr as NanoContractSignedData).value).toEqual(valueStr.value); const valueBytes: NanoContractSignedData = { type: 'bytes', - value: [Buffer.from('6e634944', 'hex'), Buffer.from('74657374', 'hex')], + ncId: Buffer.from('6e634944', 'hex'), + value: Buffer.from('74657374', 'hex'), signature: Buffer.from('74657374', 'hex'), }; const serializedBytes = serializer.serializeFromType(valueBytes, 'SignedData[bytes]'); @@ -239,13 +242,14 @@ test('SignedData', () => { valueBytes.signature ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBytes as NanoContractSignedData).value[0]).toMatchBuffer(valueBytes.value[0]); + expect((deserializedBytes as NanoContractSignedData).ncId).toMatchBuffer(valueBytes.ncId); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBytes as NanoContractSignedData).value[1]).toMatchBuffer(valueBytes.value[1]); + expect((deserializedBytes as NanoContractSignedData).value).toMatchBuffer(valueBytes.value); const valueBoolFalse: NanoContractSignedData = { type: 'bool', - value: [Buffer.from('6e634944', 'hex'), false], + ncId: Buffer.from('6e634944', 'hex'), + value: false, signature: Buffer.from('74657374', 'hex'), }; const serializedBoolFalse = serializer.serializeFromType(valueBoolFalse, 'SignedData[bool]'); @@ -260,16 +264,17 @@ test('SignedData', () => { valueBoolFalse.signature ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBoolFalse as NanoContractSignedData).value[0]).toMatchBuffer( - valueBoolFalse.value[0] + expect((deserializedBoolFalse as NanoContractSignedData).ncId).toMatchBuffer( + valueBoolFalse.ncId ); - expect((deserializedBoolFalse as NanoContractSignedData).value[1]).toEqual( - valueBoolFalse.value[1] + expect((deserializedBoolFalse as NanoContractSignedData).value).toEqual( + valueBoolFalse.value ); const valueBoolTrue: NanoContractSignedData = { type: 'bool', - value: [Buffer.from('6e634944', 'hex'), true], + ncId: Buffer.from('6e634944', 'hex'), + value: true, signature: Buffer.from('74657374', 'hex'), }; const serializedBoolTrue = serializer.serializeFromType(valueBoolTrue, 'SignedData[bool]'); @@ -284,14 +289,15 @@ test('SignedData', () => { valueBoolTrue.signature ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBoolTrue as NanoContractSignedData).value[0]).toMatchBuffer( - valueBoolTrue.value[0] + expect((deserializedBoolTrue as NanoContractSignedData).ncId).toMatchBuffer( + valueBoolTrue.ncId ); - expect((deserializedBoolTrue as NanoContractSignedData).value[1]).toEqual(valueBoolTrue.value[1]); + expect((deserializedBoolTrue as NanoContractSignedData).value).toEqual(valueBoolTrue.value); const valueVarInt: NanoContractSignedData = { type: 'VarInt', - value: [Buffer.from('6e634944', 'hex'), 300n], + ncId: Buffer.from('6e634944', 'hex'), + value: 300n, signature: Buffer.from('74657374', 'hex'), }; const serializedVarInt = serializer.serializeFromType(valueVarInt, 'SignedData[VarInt]'); @@ -306,10 +312,10 @@ test('SignedData', () => { valueVarInt.signature ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedVarInt as NanoContractSignedData).value[0]).toMatchBuffer( - valueVarInt.value[0] + expect((deserializedVarInt as NanoContractSignedData).ncId).toMatchBuffer( + valueVarInt.ncId ); - expect((deserializedVarInt as NanoContractSignedData).value[1]).toEqual(valueVarInt.value[1]); + expect((deserializedVarInt as NanoContractSignedData).value).toEqual(valueVarInt.value); }); test('Address', () => { diff --git a/__tests__/nano_contracts/methodArg.test.ts b/__tests__/nano_contracts/methodArg.test.ts index 56c197293..86c98994a 100644 --- a/__tests__/nano_contracts/methodArg.test.ts +++ b/__tests__/nano_contracts/methodArg.test.ts @@ -29,10 +29,10 @@ describe('fromApiInput', () => { Buffer.from([0x74, 0x65, 0x73, 0x74]) ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( Buffer.from([0x6e, 0x63, 0x49, 0x44]) ); - expect((arg.value as NanoContractSignedData).value[1]).toEqual(300); + expect((arg.value as NanoContractSignedData).value).toEqual(300); }); it('should read SignedData[VarInt]', () => { @@ -55,10 +55,10 @@ describe('fromApiInput', () => { Buffer.from([0x74, 0x65, 0x73, 0x74]) ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( Buffer.from([0x6e, 0x63, 0x49, 0x44]) ); - expect((arg.value as NanoContractSignedData).value[1]).toEqual(300n); + expect((arg.value as NanoContractSignedData).value).toEqual(300n); }); it('should read SignedData[str]', () => { @@ -81,10 +81,10 @@ describe('fromApiInput', () => { Buffer.from([0x74, 0x65, 0x73, 0x74]) ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( Buffer.from([0x6e, 0x63, 0x49, 0x44]) ); - expect((arg.value as NanoContractSignedData).value[1]).toEqual('test'); + expect((arg.value as NanoContractSignedData).value).toEqual('test'); }); it('should read SignedData[bytes]', () => { @@ -107,11 +107,11 @@ describe('fromApiInput', () => { Buffer.from([0x74, 0x65, 0x73, 0x74]) ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( Buffer.from([0x6e, 0x63, 0x49, 0x44]) ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[1]).toMatchBuffer( + expect((arg.value as NanoContractSignedData).value).toMatchBuffer( Buffer.from([0x74, 0x65, 0x73, 0x74]) ); }); @@ -136,10 +136,10 @@ describe('fromApiInput', () => { Buffer.from([0x74, 0x65, 0x73, 0x74]) ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( Buffer.from([0x6e, 0x63, 0x49, 0x44]) ); - expect((arg.value as NanoContractSignedData).value[1]).toEqual(true); + expect((arg.value as NanoContractSignedData).value).toEqual(true); }); it('should read false SignedData[bool]', () => { @@ -162,10 +162,10 @@ describe('fromApiInput', () => { Buffer.from([0x74, 0x65, 0x73, 0x74]) ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).value[0]).toMatchBuffer( + expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( Buffer.from([0x6e, 0x63, 0x49, 0x44]) ); - expect((arg.value as NanoContractSignedData).value[1]).toEqual(false); + expect((arg.value as NanoContractSignedData).value).toEqual(false); }); it('should read str values', () => { diff --git a/src/nano_contracts/deserializer.ts b/src/nano_contracts/deserializer.ts index 6323dfd09..5dafd3994 100644 --- a/src/nano_contracts/deserializer.ts +++ b/src/nano_contracts/deserializer.ts @@ -312,7 +312,8 @@ class Deserializer { return { value: { type, - value: [ncId, parsed as NanoContractArgumentSingleType], + ncId, + value: parsed as NanoContractArgumentSingleType, signature: parsedSignature as Buffer, }, bytesRead: bytesReadFromContractId + bytesReadFromValue + bytesReadFromSignature, diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index 0871046ff..fde8d51fe 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -177,13 +177,14 @@ const SignedDataApiInputScheme = z ) .transform((value, ctx) => { const signature = Buffer.from(value[0], 'hex'); - const ncID = Buffer.from(value[1], 'hex'); + const ncId = Buffer.from(value[1], 'hex'); const type = value[3]; const refinedValue = refineSingleValue(ctx, value[2], type); const ret: NanoContractSignedData = { signature, type, - value: [ncID, refinedValue], + ncId, + value: refinedValue, }; return ret; }); @@ -322,8 +323,8 @@ export class NanoContractMethodArgument { type: this.type, parsed: [ data.signature.toString('hex'), - data.value[0].toString('hex'), - prepSingleValue(data.type, data.value[1]), + data.ncId.toString('hex'), + prepSingleValue(data.type, data.value), this.type, ].join(','), }; diff --git a/src/nano_contracts/serializer.ts b/src/nano_contracts/serializer.ts index c3f795fe0..4cd4b9474 100644 --- a/src/nano_contracts/serializer.ts +++ b/src/nano_contracts/serializer.ts @@ -76,8 +76,9 @@ class Serializer { return this.fromOptional(value, innerType); case 'SignedData': return this.fromSignedData(value as NanoContractSignedData, innerType); - case 'RawSignedData': case 'Tuple': + return this.fromTuple(value as NanoContractArgumentType[], innerType); + case 'RawSignedData': throw new Error('Not implemented'); default: throw new Error('Invalid type'); @@ -226,9 +227,10 @@ class Serializer { throw new Error('type mismatch'); } - const ncId = this.serializeFromType(signedValue.value[0], 'bytes'); - ret.push(ncId); - const serialized = this.serializeFromType(signedValue.value[1], signedValue.type); + const serialized = this.serializeFromType( + [signedValue.ncId, signedValue.value], + `Tuple[ContractId, ${type}]`, + ); ret.push(serialized); const signature = this.serializeFromType(signedValue.signature, 'bytes'); ret.push(signature); @@ -262,6 +264,18 @@ class Serializer { return Buffer.concat(ret); } + + fromTuple(value: NanoContractArgumentType[], typeStr: string): Buffer { + const typeArr = typeStr.split(',').map(t => t.trim()); + const serialized: Buffer[] = []; + if (typeArr.length !== value.length) { + throw new Error('Tuple value with length mismatch, required ') + } + for (const [index, type] of typeArr.entries()) { + serialized.push(this.serializeFromType(value[index], type)); + } + return Buffer.concat(serialized); + } } export default Serializer; diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 92528ad06..7c0a65cf2 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -42,7 +42,8 @@ export type NanoContractSignedDataInnerType = [Buffer, NanoContractArgumentSingl export const NanoContractSignedDataSchema = z.object({ type: z.string(), signature: z.instanceof(Buffer), - value: z.tuple([z.instanceof(Buffer), NanoContractArgumentSingleSchema]), + value: NanoContractArgumentSingleSchema, + ncId: z.instanceof(Buffer), }); export type NanoContractSignedData = z.output; diff --git a/src/nano_contracts/utils.ts b/src/nano_contracts/utils.ts index bd7c13855..5a684b565 100644 --- a/src/nano_contracts/utils.ts +++ b/src/nano_contracts/utils.ts @@ -30,6 +30,7 @@ import { } from './types'; import { NANO_CONTRACTS_INITIALIZE_METHOD } from '../constants'; import { NanoContractMethodArgument } from './methodArg'; +import leb128 from '../utils/leb128'; export function getContainerInternalType( type: string @@ -131,6 +132,20 @@ export const getOracleBuffer = (oracle: string, network: Network): Buffer => { * @param wallet Hathor Wallet object */ export const getOracleInputData = async ( + oracleData: Buffer, + contractId: Buffer, + resultSerialized: Buffer, + wallet: HathorWallet +) => { + const actualValue = Buffer.concat([ + leb128.encodeUnsigned(contractId.length), + contractId, + resultSerialized, + ]); + return unsafeGetOracleInputData(oracleData, actualValue, wallet); +}; + +export const unsafeGetOracleInputData = async ( oracleData: Buffer, resultSerialized: Buffer, wallet: HathorWallet From 090bfcbf7b719d6a6173f40acb40f3fce11726fe Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Wed, 14 May 2025 19:31:22 -0300 Subject: [PATCH 16/23] feat: remove ncId from SignedData serialization --- .../integration/nanocontracts/bet.test.ts | 13 ++-- __tests__/nano_contracts/deserializer.test.ts | 28 +------- __tests__/nano_contracts/methodArg.test.ts | 51 +++----------- __tests__/nano_contracts/serializer.test.ts | 50 +++++--------- src/nano_contracts/deserializer.ts | 46 ++----------- src/nano_contracts/methodArg.ts | 69 ++----------------- src/nano_contracts/serializer.ts | 41 +---------- src/nano_contracts/types.ts | 13 +--- src/nano_contracts/utils.ts | 9 +-- 9 files changed, 60 insertions(+), 260 deletions(-) diff --git a/__tests__/integration/nanocontracts/bet.test.ts b/__tests__/integration/nanocontracts/bet.test.ts index d1805f4ba..25cddcb6c 100644 --- a/__tests__/integration/nanocontracts/bet.test.ts +++ b/__tests__/integration/nanocontracts/bet.test.ts @@ -95,6 +95,7 @@ describe('full cycle of bet nano contract', () => { // Create NC const oracleData = getOracleBuffer(address1, network); + console.debug('Call to initialize'); const tx1 = await wallet.createAndSendNanoContractTransaction( NANO_CONTRACTS_INITIALIZE_METHOD, address0, @@ -177,6 +178,7 @@ describe('full cycle of bet nano contract', () => { ).rejects.toThrow(NanoContractTransactionError); // Bet 100 to address 2 + console.debug('First call to bet'); const txBet = await wallet.createAndSendNanoContractTransaction('bet', address2, { ncId: tx1.hash, args: [address2, '1x0'], @@ -232,6 +234,7 @@ describe('full cycle of bet nano contract', () => { // Bet 200 to address 3 const address3 = await wallet.getAddressAtIndex(3); + console.debug('Second call to bet'); const txBet2 = await wallet.createAndSendNanoContractTransaction('bet', address3, { ncId: tx1.hash, args: [address3, '2x0'], @@ -324,10 +327,11 @@ describe('full cycle of bet nano contract', () => { const nanoSerializer = new Serializer(network); const result = '1x0'; const resultSerialized = nanoSerializer.serializeFromType(result, 'str'); - const inputData = await getOracleInputData(oracleData, Buffer.from(tx1.hash, 'hex'), resultSerialized, wallet); + const inputData = await getOracleInputData(oracleData, tx1.hash, resultSerialized, wallet); + console.debug('Call to set_result'); const txSetResult = await wallet.createAndSendNanoContractTransaction('set_result', address1, { ncId: tx1.hash, - args: [`${bufferToHex(inputData)},${bufferToHex(tx1.hash)},${result},str`], + args: [`${bufferToHex(inputData)},${result},str`], }); await checkTxValid(wallet, txSetResult); txIds.push(txSetResult.hash); @@ -373,11 +377,10 @@ describe('full cycle of bet nano contract', () => { (txSetResultParser.parsedArgs[0].value as NanoContractSignedData).ncId // @ts-expect-error toMatchBuffer is defined in setupTests.js ).toMatchBuffer(Buffer.from(tx1.hash, 'hex')); - expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value).toEqual( - result - ); + expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value).toEqual(result); // Try to withdraw to address 2, success + console.debug('Call to withdraw'); const txWithdrawal = await wallet.createAndSendNanoContractTransaction('withdraw', address2, { ncId: tx1.hash, actions: [ diff --git a/__tests__/nano_contracts/deserializer.test.ts b/__tests__/nano_contracts/deserializer.test.ts index e85266741..fe3ddcaf3 100644 --- a/__tests__/nano_contracts/deserializer.test.ts +++ b/__tests__/nano_contracts/deserializer.test.ts @@ -189,7 +189,6 @@ test('SignedData', () => { const valueInt: NanoContractSignedData = { type: 'int', value: 300, - ncId: Buffer.from('6e634944', 'hex'), signature: Buffer.from('74657374', 'hex'), }; const serializedInt = serializer.serializeFromType(valueInt, 'SignedData[int]'); @@ -201,13 +200,10 @@ test('SignedData', () => { expect((deserializedInt as NanoContractSignedData).type).toEqual(valueInt.type); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((deserializedInt as NanoContractSignedData).signature).toMatchBuffer(valueInt.signature); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedInt as NanoContractSignedData).ncId).toMatchBuffer(valueInt.ncId); expect((deserializedInt as NanoContractSignedData).value).toEqual(valueInt.value); const valueStr: NanoContractSignedData = { type: 'str', - ncId: Buffer.from('6e634944', 'hex'), value: 'test', signature: Buffer.from('74657374', 'hex'), }; @@ -220,13 +216,10 @@ test('SignedData', () => { expect((deserializedStr as NanoContractSignedData).type).toEqual(valueStr.type); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((deserializedStr as NanoContractSignedData).signature).toMatchBuffer(valueStr.signature); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedStr as NanoContractSignedData).ncId).toMatchBuffer(valueStr.ncId); expect((deserializedStr as NanoContractSignedData).value).toEqual(valueStr.value); const valueBytes: NanoContractSignedData = { type: 'bytes', - ncId: Buffer.from('6e634944', 'hex'), value: Buffer.from('74657374', 'hex'), signature: Buffer.from('74657374', 'hex'), }; @@ -242,13 +235,10 @@ test('SignedData', () => { valueBytes.signature ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBytes as NanoContractSignedData).ncId).toMatchBuffer(valueBytes.ncId); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((deserializedBytes as NanoContractSignedData).value).toMatchBuffer(valueBytes.value); const valueBoolFalse: NanoContractSignedData = { type: 'bool', - ncId: Buffer.from('6e634944', 'hex'), value: false, signature: Buffer.from('74657374', 'hex'), }; @@ -263,17 +253,10 @@ test('SignedData', () => { expect((deserializedBoolFalse as NanoContractSignedData).signature).toMatchBuffer( valueBoolFalse.signature ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBoolFalse as NanoContractSignedData).ncId).toMatchBuffer( - valueBoolFalse.ncId - ); - expect((deserializedBoolFalse as NanoContractSignedData).value).toEqual( - valueBoolFalse.value - ); + expect((deserializedBoolFalse as NanoContractSignedData).value).toEqual(valueBoolFalse.value); const valueBoolTrue: NanoContractSignedData = { type: 'bool', - ncId: Buffer.from('6e634944', 'hex'), value: true, signature: Buffer.from('74657374', 'hex'), }; @@ -288,15 +271,10 @@ test('SignedData', () => { expect((deserializedBoolTrue as NanoContractSignedData).signature).toMatchBuffer( valueBoolTrue.signature ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedBoolTrue as NanoContractSignedData).ncId).toMatchBuffer( - valueBoolTrue.ncId - ); expect((deserializedBoolTrue as NanoContractSignedData).value).toEqual(valueBoolTrue.value); const valueVarInt: NanoContractSignedData = { type: 'VarInt', - ncId: Buffer.from('6e634944', 'hex'), value: 300n, signature: Buffer.from('74657374', 'hex'), }; @@ -311,10 +289,6 @@ test('SignedData', () => { expect((deserializedVarInt as NanoContractSignedData).signature).toMatchBuffer( valueVarInt.signature ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((deserializedVarInt as NanoContractSignedData).ncId).toMatchBuffer( - valueVarInt.ncId - ); expect((deserializedVarInt as NanoContractSignedData).value).toEqual(valueVarInt.value); }); diff --git a/__tests__/nano_contracts/methodArg.test.ts b/__tests__/nano_contracts/methodArg.test.ts index 86c98994a..c912c7474 100644 --- a/__tests__/nano_contracts/methodArg.test.ts +++ b/__tests__/nano_contracts/methodArg.test.ts @@ -13,7 +13,7 @@ describe('fromApiInput', () => { const arg = NanoContractMethodArgument.fromApiInput( 'a-test', 'SignedData[int]', - '74657374,6e634944,300,int' + '74657374,300,int' ); expect(arg).toMatchObject({ name: 'a-test', @@ -21,25 +21,20 @@ describe('fromApiInput', () => { value: { type: 'int', signature: expect.anything(), - value: expect.anything(), + value: 300, }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( Buffer.from([0x74, 0x65, 0x73, 0x74]) ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( - Buffer.from([0x6e, 0x63, 0x49, 0x44]) - ); - expect((arg.value as NanoContractSignedData).value).toEqual(300); }); it('should read SignedData[VarInt]', () => { const arg = NanoContractMethodArgument.fromApiInput( 'a-test', 'SignedData[VarInt]', - '74657374,6e634944,300,VarInt' + '74657374,300,VarInt' ); expect(arg).toMatchObject({ name: 'a-test', @@ -47,25 +42,20 @@ describe('fromApiInput', () => { value: { type: 'VarInt', signature: expect.anything(), - value: expect.anything(), + value: 300n, }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( Buffer.from([0x74, 0x65, 0x73, 0x74]) ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( - Buffer.from([0x6e, 0x63, 0x49, 0x44]) - ); - expect((arg.value as NanoContractSignedData).value).toEqual(300n); }); it('should read SignedData[str]', () => { const arg = NanoContractMethodArgument.fromApiInput( 'a-test', 'SignedData[str]', - '74657374,6e634944,test,str' + '74657374,test,str' ); expect(arg).toMatchObject({ name: 'a-test', @@ -73,25 +63,20 @@ describe('fromApiInput', () => { value: { type: 'str', signature: expect.anything(), - value: expect.anything(), + value: 'test', }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( Buffer.from([0x74, 0x65, 0x73, 0x74]) ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( - Buffer.from([0x6e, 0x63, 0x49, 0x44]) - ); - expect((arg.value as NanoContractSignedData).value).toEqual('test'); }); it('should read SignedData[bytes]', () => { const arg = NanoContractMethodArgument.fromApiInput( 'a-test', 'SignedData[bytes]', - '74657374,6e634944,74657374,bytes' + '74657374,74657374,bytes' ); expect(arg).toMatchObject({ name: 'a-test', @@ -107,10 +92,6 @@ describe('fromApiInput', () => { Buffer.from([0x74, 0x65, 0x73, 0x74]) ); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( - Buffer.from([0x6e, 0x63, 0x49, 0x44]) - ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((arg.value as NanoContractSignedData).value).toMatchBuffer( Buffer.from([0x74, 0x65, 0x73, 0x74]) ); @@ -120,7 +101,7 @@ describe('fromApiInput', () => { const arg = NanoContractMethodArgument.fromApiInput( 'a-test', 'SignedData[bool]', - '74657374,6e634944,true,bool' + '74657374,true,bool' ); expect(arg).toMatchObject({ name: 'a-test', @@ -128,25 +109,20 @@ describe('fromApiInput', () => { value: { type: 'bool', signature: expect.anything(), - value: expect.anything(), + value: true, }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( Buffer.from([0x74, 0x65, 0x73, 0x74]) ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( - Buffer.from([0x6e, 0x63, 0x49, 0x44]) - ); - expect((arg.value as NanoContractSignedData).value).toEqual(true); }); it('should read false SignedData[bool]', () => { const arg = NanoContractMethodArgument.fromApiInput( 'a-test', 'SignedData[bool]', - '74657374,6e634944,false,bool' + '74657374,false,bool' ); expect(arg).toMatchObject({ name: 'a-test', @@ -154,18 +130,13 @@ describe('fromApiInput', () => { value: { type: 'bool', signature: expect.anything(), - value: expect.anything(), + value: false, }, }); // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. expect((arg.value as NanoContractSignedData).signature).toMatchBuffer( Buffer.from([0x74, 0x65, 0x73, 0x74]) ); - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. - expect((arg.value as NanoContractSignedData).ncId).toMatchBuffer( - Buffer.from([0x6e, 0x63, 0x49, 0x44]) - ); - expect((arg.value as NanoContractSignedData).value).toEqual(false); }); it('should read str values', () => { diff --git a/__tests__/nano_contracts/serializer.test.ts b/__tests__/nano_contracts/serializer.test.ts index 24342a430..da08a2362 100644 --- a/__tests__/nano_contracts/serializer.test.ts +++ b/__tests__/nano_contracts/serializer.test.ts @@ -94,16 +94,14 @@ test('SignedData', () => { { signature: Buffer.from('74657374', 'hex'), type: 'int', - value: [Buffer.from('6e634944', 'hex'), 300], + value: 300, }, 'int' - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. ) + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. ).toMatchBuffer( - // 4 + ncId + value + 4 + test - Buffer.from([ - 0x04, 0x6e, 0x63, 0x49, 0x44, 0x00, 0x00, 0x01, 0x2c, 0x04, 0x74, 0x65, 0x73, 0x74, - ]) + // value + 4 + test + Buffer.from([0x00, 0x00, 0x01, 0x2c, 0x04, 0x74, 0x65, 0x73, 0x74]) ); expect( @@ -111,70 +109,60 @@ test('SignedData', () => { { signature: Buffer.from('74657374', 'hex'), type: 'VarInt', - value: [Buffer.from('6e634944', 'hex'), 300n], + value: 300n, }, 'VarInt' - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. ) - ).toMatchBuffer( - Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0xac, 0x02, 0x04, 0x74, 0x65, 0x73, 0x74]) - ); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ).toMatchBuffer(Buffer.from([0xac, 0x02, 0x04, 0x74, 0x65, 0x73, 0x74])); expect( serializer.fromSignedData( { signature: Buffer.from('74657374', 'hex'), type: 'str', - value: [Buffer.from('6e634944', 'hex'), 'test'], + value: 'test', }, 'str' - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. ) - ).toMatchBuffer( - Buffer.from([ - 0x04, 0x6e, 0x63, 0x49, 0x44, 0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74, - ]) - ); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ).toMatchBuffer(Buffer.from([0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74])); expect( serializer.fromSignedData( { signature: Buffer.from('74657374', 'hex'), type: 'bytes', - value: [Buffer.from('6e634944', 'hex'), Buffer.from('74657374', 'hex')], + value: Buffer.from('74657374', 'hex'), }, 'bytes' - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. ) - ).toMatchBuffer( - Buffer.from([ - 0x04, 0x6e, 0x63, 0x49, 0x44, 0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74, - ]) - ); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ).toMatchBuffer(Buffer.from([0x04, 0x74, 0x65, 0x73, 0x74, 0x04, 0x74, 0x65, 0x73, 0x74])); expect( serializer.fromSignedData( { signature: Buffer.from('74657374', 'hex'), type: 'bool', - value: [Buffer.from('6e634944', 'hex'), false], + value: false, }, 'bool' - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. ) - ).toMatchBuffer(Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ).toMatchBuffer(Buffer.from([0x00, 0x04, 0x74, 0x65, 0x73, 0x74])); expect( serializer.fromSignedData( { signature: Buffer.from('74657374', 'hex'), type: 'bool', - value: [Buffer.from('6e634944', 'hex'), true], + value: true, }, 'bool' - // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. ) - ).toMatchBuffer(Buffer.from([0x04, 0x6e, 0x63, 0x49, 0x44, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74])); + // @ts-expect-error: toMatchBuffer is defined in our setupTests.js so the type check fails. + ).toMatchBuffer(Buffer.from([0x01, 0x04, 0x74, 0x65, 0x73, 0x74])); }); test('VarInt', () => { diff --git a/src/nano_contracts/deserializer.ts b/src/nano_contracts/deserializer.ts index 5dafd3994..5a33819d8 100644 --- a/src/nano_contracts/deserializer.ts +++ b/src/nano_contracts/deserializer.ts @@ -14,7 +14,6 @@ import { BufferROExtract, NanoContractSignedData, NanoContractArgumentSingleType, - NanoContractRawSignedData, } from './types'; import { OutputValueType } from '../types'; import { NC_ARGS_MAX_BYTES_LENGTH } from '../constants'; @@ -79,9 +78,9 @@ class Deserializer { switch (containerType) { case 'Optional': return this.toOptional(buf, internalType); + case 'RawSignedData': case 'SignedData': return this.toSignedData(buf, internalType); - case 'RawSignedData': case 'Tuple': throw new Error('Not implemented yet'); default: @@ -288,38 +287,6 @@ class Deserializer { }; } - toSignedData(signedData: Buffer, type: string): BufferROExtract { - // The SignedData is serialized as `ContractId+data+Signature` - - // Reading ContractId - const ncIdResult = this.deserializeFromType(signedData, 'ContractId'); - const ncId = ncIdResult.value as Buffer; - const bytesReadFromContractId = ncIdResult.bytesRead; - - const buf = signedData.subarray(bytesReadFromContractId); - - // Reading argument - const parseResult = this.deserializeFromType(buf, type); - const parsed = parseResult.value; - const bytesReadFromValue = parseResult.bytesRead; - - // Reading signature as bytes - const { value: parsedSignature, bytesRead: bytesReadFromSignature } = this.deserializeFromType( - buf.subarray(bytesReadFromValue), - 'bytes' - ); - - return { - value: { - type, - ncId, - value: parsed as NanoContractArgumentSingleType, - signature: parsedSignature as Buffer, - }, - bytesRead: bytesReadFromContractId + bytesReadFromValue + bytesReadFromSignature, - }; - } - /** * Deserialize a signed value * @@ -332,18 +299,19 @@ class Deserializer { * @memberof Deserializer * @inner */ - toRawSignedData(signedData: Buffer, type: string): BufferROExtract { - // RawSignData[T] is serialized as Serialize(T)+Serialize(sign(T)) where sign() returns a byte str - // Which means we can parse the T argument, then read the bytes after. + toSignedData(signedData: Buffer, type: string): BufferROExtract { + // The SignedData is serialized as `ContractId+data+Signature` // Reading argument const parseResult = this.deserializeFromType(signedData, type); const parsed = parseResult.value; const bytesReadFromValue = parseResult.bytesRead; - // Reading signature + const buf = signedData.subarray(bytesReadFromValue); + + // Reading signature as bytes const { value: parsedSignature, bytesRead: bytesReadFromSignature } = this.deserializeFromType( - signedData.subarray(bytesReadFromValue), + buf, 'bytes' ); diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index fde8d51fe..e3079e610 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -10,7 +10,6 @@ import { NanoContractArgumentSingleType, NanoContractArgumentType, NanoContractParsedArgument, - NanoContractRawSignedData, NanoContractSignedData, BufferROExtract, NanoContractArgumentApiInputType, @@ -165,35 +164,6 @@ const OptionalApiInputScheme = z * returns an instance of NanoContractSignedData */ const SignedDataApiInputScheme = z - .string() - .transform(value => value.split(',')) - .pipe( - z.tuple([ - z.string().regex(/^[0-9A-Fa-f]+$/), - z.string().regex(/^[0-9A-Fa-f]+$/), - z.string(), - z.string(), - ]) - ) - .transform((value, ctx) => { - const signature = Buffer.from(value[0], 'hex'); - const ncId = Buffer.from(value[1], 'hex'); - const type = value[3]; - const refinedValue = refineSingleValue(ctx, value[2], type); - const ret: NanoContractSignedData = { - signature, - type, - ncId, - value: refinedValue, - }; - return ret; - }); - -/** - * Type and value validation for RawSignedData types. - * returns an instance of NanoContractRawSignedData - */ -const RawSignedDataApiInputScheme = z .string() .transform(value => value.split(',')) .pipe(z.tuple([z.string().regex(/^[0-9A-Fa-f]+$/), z.string(), z.string()])) @@ -201,7 +171,7 @@ const RawSignedDataApiInputScheme = z const signature = Buffer.from(value[0], 'hex'); const type = value[2]; const refinedValue = refineSingleValue(ctx, value[1], type); - const ret: NanoContractRawSignedData = { + const ret: NanoContractSignedData = { signature, type, value: refinedValue, @@ -249,20 +219,14 @@ export class NanoContractMethodArgument { /** * User input and api serialized input may not be encoded in the actual value type. * - * ## SignedData - * We expect the value as a string separated by comma (,) with 4 elements - * (signature, ncID, value, type) + * ## SignedData and RawSignedData + * We expect the value as a string separated by comma (,) with 3 elements + * (signature, value, type) * Since the value is encoded as a string some special cases apply: * - bool: 'true' or 'false'. * - bytes (and any bytes encoded value): hex encoded string of the byte value. * * While the value should be the NanoContractSignedDataSchema - * - * ## RawSignedData - * We expect the value as a string separated by comma (,) with 3 elements - * (signature, value, type) - * - * While the value should be the NanoContractRawSignedDataSchema */ static fromApiInput( name: string, @@ -272,19 +236,14 @@ export class NanoContractMethodArgument { const isContainerType = getContainerType(type) !== null; if (isContainerType) { const [containerType, innerType] = getContainerInternalType(type); - if (containerType === 'SignedData') { + if (containerType === 'SignedData' || containerType === 'RawSignedData') { // Parse string SignedData into NanoContractSignedData const data = SignedDataApiInputScheme.parse(value); if (data.type !== innerType.trim()) { - throw new Error(); + throw new Error('Invalid signed data type'); } return new NanoContractMethodArgument(name, type, data); } - if (containerType === 'RawSignedData') { - // Parse string RawSignedData into NanoContractRawSignedData - const data = RawSignedDataApiInputScheme.parse(value); - return new NanoContractMethodArgument(name, type, data); - } if (containerType === 'Optional') { const data = OptionalApiInputScheme.parse([innerType, value]); return new NanoContractMethodArgument(name, type, data); @@ -316,22 +275,8 @@ export class NanoContractMethodArgument { return value; } - if (this.type.startsWith('SignedData')) { + if (this.type.startsWith('SignedData') || this.type.startsWith('RawSignedData')) { const data = this.value as NanoContractSignedData; - return { - name: this.name, - type: this.type, - parsed: [ - data.signature.toString('hex'), - data.ncId.toString('hex'), - prepSingleValue(data.type, data.value), - this.type, - ].join(','), - }; - } - - if (this.type.startsWith('RawSignedData')) { - const data = this.value as NanoContractRawSignedData; return { name: this.name, type: this.type, diff --git a/src/nano_contracts/serializer.ts b/src/nano_contracts/serializer.ts index 4cd4b9474..d68f2023c 100644 --- a/src/nano_contracts/serializer.ts +++ b/src/nano_contracts/serializer.ts @@ -8,11 +8,7 @@ import Address from '../models/address'; import Network from '../models/network'; import { signedIntToBytes, bigIntToBytes } from '../utils/buffer'; -import { - NanoContractArgumentType, - NanoContractRawSignedData, - NanoContractSignedData, -} from './types'; +import { NanoContractArgumentType, NanoContractSignedData } from './types'; import { OutputValueType } from '../types'; import leb128Util from '../utils/leb128'; import { getContainerInternalType, getContainerType } from './utils'; @@ -74,12 +70,11 @@ class Serializer { switch (containerType) { case 'Optional': return this.fromOptional(value, innerType); + case 'RawSignedData': case 'SignedData': return this.fromSignedData(value as NanoContractSignedData, innerType); case 'Tuple': return this.fromTuple(value as NanoContractArgumentType[], innerType); - case 'RawSignedData': - throw new Error('Not implemented'); default: throw new Error('Invalid type'); } @@ -227,36 +222,6 @@ class Serializer { throw new Error('type mismatch'); } - const serialized = this.serializeFromType( - [signedValue.ncId, signedValue.value], - `Tuple[ContractId, ${type}]`, - ); - ret.push(serialized); - const signature = this.serializeFromType(signedValue.signature, 'bytes'); - ret.push(signature); - - return Buffer.concat(ret); - } - - /** - * Serialize a signed value - * We expect the value as a string separated by comma (,) - * with 3 elements (inputData, value, type) - * - * The serialization will be - * [len(serializedValue)][serializedValue][inputData] - * - * @param signedValue String value with inputData, value, and type separated by comma - * - * @memberof Serializer - * @inner - */ - fromRawSignedData(signedValue: NanoContractRawSignedData, type: string): Buffer { - const ret: Buffer[] = []; - if (signedValue.type !== type) { - throw new Error('type mismatch'); - } - const serialized = this.serializeFromType(signedValue.value, signedValue.type); ret.push(serialized); const signature = this.serializeFromType(signedValue.signature, 'bytes'); @@ -269,7 +234,7 @@ class Serializer { const typeArr = typeStr.split(',').map(t => t.trim()); const serialized: Buffer[] = []; if (typeArr.length !== value.length) { - throw new Error('Tuple value with length mismatch, required ') + throw new Error('Tuple value with length mismatch, required '); } for (const [index, type] of typeArr.entries()) { serialized.push(this.serializeFromType(value[index], type)); diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 7c0a65cf2..47f36e2fa 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -43,20 +43,10 @@ export const NanoContractSignedDataSchema = z.object({ type: z.string(), signature: z.instanceof(Buffer), value: NanoContractArgumentSingleSchema, - ncId: z.instanceof(Buffer), + ncId: z.instanceof(Buffer).nullish(), }); export type NanoContractSignedData = z.output; -/** - * NanoContract RawSignedData method argument type - */ -export const NanoContractRawSignedDataSchema = z.object({ - type: z.string(), - signature: z.instanceof(Buffer), - value: NanoContractArgumentSingleSchema, -}); -export type NanoContractRawSignedData = z.output; - /** * Intermediate schema for all possible Nano contract argument type * that do not include tuple/arrays/repetition @@ -64,7 +54,6 @@ export type NanoContractRawSignedData = z.output { */ export const getOracleInputData = async ( oracleData: Buffer, - contractId: Buffer, + contractId: string, resultSerialized: Buffer, wallet: HathorWallet ) => { - const actualValue = Buffer.concat([ - leb128.encodeUnsigned(contractId.length), - contractId, - resultSerialized, - ]); + const ncId = Buffer.from(contractId, 'hex'); + const actualValue = Buffer.concat([leb128.encodeUnsigned(ncId.length), ncId, resultSerialized]); return unsafeGetOracleInputData(oracleData, actualValue, wallet); }; From cb8b8faea83658bc7e1be2e0742c9e1f7a359648 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Thu, 15 May 2025 13:22:20 -0300 Subject: [PATCH 17/23] tests: bet nano contract --- .../integration/configuration/docker-compose.yml | 1 + __tests__/integration/nanocontracts/bet.test.ts | 15 ++------------- src/nano_contracts/parser.ts | 4 ++++ src/nano_contracts/types.ts | 6 ------ 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/__tests__/integration/configuration/docker-compose.yml b/__tests__/integration/configuration/docker-compose.yml index cf7eec805..16162f339 100644 --- a/__tests__/integration/configuration/docker-compose.yml +++ b/__tests__/integration/configuration/docker-compose.yml @@ -17,6 +17,7 @@ services: "--unsafe-mode", "nano-testnet-alpha", "--data", "./tmp", "--nc-indices", + "--nc-exec-logs", "all", ] environment: HATHOR_CONFIG_YAML: privnet/conf/privnet.yml diff --git a/__tests__/integration/nanocontracts/bet.test.ts b/__tests__/integration/nanocontracts/bet.test.ts index 25cddcb6c..5af0a3bb1 100644 --- a/__tests__/integration/nanocontracts/bet.test.ts +++ b/__tests__/integration/nanocontracts/bet.test.ts @@ -352,13 +352,6 @@ describe('full cycle of bet nano contract', () => { txSetResultParser.parseAddress(); await txSetResultParser.parseArguments(); expect(txSetResultParser.address?.base58).toBe(address1); - expect(txSetResultParser.parsedArgs).toStrictEqual([ - { - name: 'result', - type: 'SignedData[str]', - parsed: `${bufferToHex(inputData)},${result},str`, - }, - ]); expect(txSetResultParser.parsedArgs).not.toBeNull(); if (txSetResultParser.parsedArgs === null) { throw new Error('Could not parse args'); @@ -371,12 +364,8 @@ describe('full cycle of bet nano contract', () => { expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).type).toEqual('str'); expect( (txSetResultParser.parsedArgs[0].value as NanoContractSignedData).signature - // @ts-expect-error toMatchBuffer is defined in setupTests.js + // @ts-expect-error toMatchBuffer is defined in setupTests.js ).toMatchBuffer(inputData); - expect( - (txSetResultParser.parsedArgs[0].value as NanoContractSignedData).ncId - // @ts-expect-error toMatchBuffer is defined in setupTests.js - ).toMatchBuffer(Buffer.from(tx1.hash, 'hex')); expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value).toEqual(result); // Try to withdraw to address 2, success @@ -404,7 +393,7 @@ describe('full cycle of bet nano contract', () => { const txWithdrawalParser = new NanoContractTransactionParser( blueprintId, - 'set_result', + 'withdraw', txWithdrawalData.tx.nc_pubkey, network, txWithdrawalData.tx.nc_args diff --git a/src/nano_contracts/parser.ts b/src/nano_contracts/parser.ts index d171403d4..1833ab1df 100644 --- a/src/nano_contracts/parser.ts +++ b/src/nano_contracts/parser.ts @@ -95,6 +95,10 @@ class NanoContractTransactionParser { throw new NanoContractTransactionParseError(`Number of arguments do not match blueprint.`); } + if (methodArgs.length === 0) { + return; + } + for (const arg of methodArgs) { let parsed: NanoContractMethodArgument; let size: number; diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 47f36e2fa..45b2ee41e 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -30,12 +30,6 @@ export const NanoContractArgumentSingleSchema = z.union([ ]); export type NanoContractArgumentSingleType = z.output; -/** - * A `SignedData` value is the tuple `[ContractId, Value]` - * which is parsed as `[Buffer, NanoContractArgumentSingleType]` - */ -export type NanoContractSignedDataInnerType = [Buffer, NanoContractArgumentSingleType]; - /** * NanoContract SignedData method argument type */ From 3bc3831182a7fa46f468cf36d3d007d48375a1bc Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Thu, 15 May 2025 14:15:12 -0300 Subject: [PATCH 18/23] chore: docstrings --- src/nano_contracts/deserializer.ts | 14 ++++++++------ src/nano_contracts/serializer.ts | 18 ++++++++++++++++++ src/nano_contracts/utils.ts | 10 ++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/nano_contracts/deserializer.ts b/src/nano_contracts/deserializer.ts index 5a33819d8..0a59ebee8 100644 --- a/src/nano_contracts/deserializer.ts +++ b/src/nano_contracts/deserializer.ts @@ -82,7 +82,7 @@ class Deserializer { case 'SignedData': return this.toSignedData(buf, internalType); case 'Tuple': - throw new Error('Not implemented yet'); + return this.toTuple(buf, internalType); default: throw new Error('Invalid type.'); } @@ -326,21 +326,23 @@ class Deserializer { } /** - * Deserialize string value + * Deserialize tuple of values. + * It does not support chained container types, meaning Tuple[Dict[str,str]] should not happen. * - * @param {Buffer} buf Value to deserialize + * @param buf Value to deserialize + * @param type Comma separated types, e.g. `str,int,VarInt` * * @memberof Deserializer * @inner */ - toTuple(buf: Buffer, type: string): BufferROExtract> { + toTuple(buf: Buffer, type: string): BufferROExtract { const typeArr = type.split(',').map(s => s.trim()); - const tupleValues: NanoContractArgumentType[] = []; + const tupleValues: NanoContractArgumentSingleType[] = []; let bytesReadTotal = 0; let tupleBuf = buf.subarray(); for (const t of typeArr) { const result = this.deserializeFromType(tupleBuf, t); - tupleValues.push(result.value); + tupleValues.push(result.value as NanoContractArgumentSingleType); bytesReadTotal += result.bytesRead; tupleBuf = tupleBuf.subarray(result.bytesRead); } diff --git a/src/nano_contracts/serializer.ts b/src/nano_contracts/serializer.ts index d68f2023c..701bbb856 100644 --- a/src/nano_contracts/serializer.ts +++ b/src/nano_contracts/serializer.ts @@ -230,6 +230,24 @@ class Serializer { return Buffer.concat(ret); } + /** + * Serialize a tuple of values + * + * @param value List of values to serialize + * @param typeStr Comma separated list of types e.g. `str,int,VarInt` + * + * @example + * ``` + * const serializer = Serializer(new Network('testnet')); + * + * const type = 'Tuple[str,int]'; + * const typeStr = 'str,int'; + * const buf = serializer.fromTuple(['1x0', 5], typeStr); + * ``` + * + * @memberof Serializer + * @inner + */ fromTuple(value: NanoContractArgumentType[], typeStr: string): Buffer { const typeArr = typeStr.split(',').map(t => t.trim()); const serialized: Buffer[] = []; diff --git a/src/nano_contracts/utils.ts b/src/nano_contracts/utils.ts index 9f875f3ed..ee2934253 100644 --- a/src/nano_contracts/utils.ts +++ b/src/nano_contracts/utils.ts @@ -128,6 +128,7 @@ export const getOracleBuffer = (oracle: string, network: Network): Buffer => { * Get oracle input data * * @param oracleData Oracle data + * @param contractId Id of the nano contract being invoked * @param resultSerialized Result to sign with oracle data already serialized * @param wallet Hathor Wallet object */ @@ -139,9 +140,18 @@ export const getOracleInputData = async ( ) => { const ncId = Buffer.from(contractId, 'hex'); const actualValue = Buffer.concat([leb128.encodeUnsigned(ncId.length), ncId, resultSerialized]); + console.log(`[oracle data to sign for inputData]: ${actualValue.toString('hex')}`); return unsafeGetOracleInputData(oracleData, actualValue, wallet); }; +/** + * [unsafe] Get oracle input data, signs received data raw. + * This is meant to be used for RawSignedData + * + * @param oracleData Oracle data + * @param resultSerialized Result to sign with oracle data already serialized + * @param wallet Hathor Wallet object + */ export const unsafeGetOracleInputData = async ( oracleData: Buffer, resultSerialized: Buffer, From bece292e8545608573b6b2ba690903605eb92767 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Thu, 15 May 2025 14:16:19 -0300 Subject: [PATCH 19/23] chore: remove console log --- src/nano_contracts/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nano_contracts/utils.ts b/src/nano_contracts/utils.ts index ee2934253..16057d082 100644 --- a/src/nano_contracts/utils.ts +++ b/src/nano_contracts/utils.ts @@ -140,7 +140,6 @@ export const getOracleInputData = async ( ) => { const ncId = Buffer.from(contractId, 'hex'); const actualValue = Buffer.concat([leb128.encodeUnsigned(ncId.length), ncId, resultSerialized]); - console.log(`[oracle data to sign for inputData]: ${actualValue.toString('hex')}`); return unsafeGetOracleInputData(oracleData, actualValue, wallet); }; From 5732f040dd7cf495985e95047424f7095ef7d567 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Thu, 15 May 2025 14:18:40 -0300 Subject: [PATCH 20/23] chore: linter changes --- __tests__/integration/nanocontracts/bet.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/integration/nanocontracts/bet.test.ts b/__tests__/integration/nanocontracts/bet.test.ts index 5af0a3bb1..467f116d2 100644 --- a/__tests__/integration/nanocontracts/bet.test.ts +++ b/__tests__/integration/nanocontracts/bet.test.ts @@ -364,7 +364,7 @@ describe('full cycle of bet nano contract', () => { expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).type).toEqual('str'); expect( (txSetResultParser.parsedArgs[0].value as NanoContractSignedData).signature - // @ts-expect-error toMatchBuffer is defined in setupTests.js + // @ts-expect-error toMatchBuffer is defined in setupTests.js ).toMatchBuffer(inputData); expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value).toEqual(result); From 845f510bb49cdb7d12c1c488478f592435e43a7c Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Fri, 16 May 2025 16:14:26 -0300 Subject: [PATCH 21/23] chore: review changes --- .../integration/nanocontracts/bet.test.ts | 5 ----- src/nano_contracts/methodArg.ts | 13 ++++--------- src/nano_contracts/types.ts | 18 +++++++++++------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/__tests__/integration/nanocontracts/bet.test.ts b/__tests__/integration/nanocontracts/bet.test.ts index 467f116d2..e4fc21cbe 100644 --- a/__tests__/integration/nanocontracts/bet.test.ts +++ b/__tests__/integration/nanocontracts/bet.test.ts @@ -95,7 +95,6 @@ describe('full cycle of bet nano contract', () => { // Create NC const oracleData = getOracleBuffer(address1, network); - console.debug('Call to initialize'); const tx1 = await wallet.createAndSendNanoContractTransaction( NANO_CONTRACTS_INITIALIZE_METHOD, address0, @@ -178,7 +177,6 @@ describe('full cycle of bet nano contract', () => { ).rejects.toThrow(NanoContractTransactionError); // Bet 100 to address 2 - console.debug('First call to bet'); const txBet = await wallet.createAndSendNanoContractTransaction('bet', address2, { ncId: tx1.hash, args: [address2, '1x0'], @@ -234,7 +232,6 @@ describe('full cycle of bet nano contract', () => { // Bet 200 to address 3 const address3 = await wallet.getAddressAtIndex(3); - console.debug('Second call to bet'); const txBet2 = await wallet.createAndSendNanoContractTransaction('bet', address3, { ncId: tx1.hash, args: [address3, '2x0'], @@ -328,7 +325,6 @@ describe('full cycle of bet nano contract', () => { const result = '1x0'; const resultSerialized = nanoSerializer.serializeFromType(result, 'str'); const inputData = await getOracleInputData(oracleData, tx1.hash, resultSerialized, wallet); - console.debug('Call to set_result'); const txSetResult = await wallet.createAndSendNanoContractTransaction('set_result', address1, { ncId: tx1.hash, args: [`${bufferToHex(inputData)},${result},str`], @@ -369,7 +365,6 @@ describe('full cycle of bet nano contract', () => { expect((txSetResultParser.parsedArgs[0].value as NanoContractSignedData).value).toEqual(result); // Try to withdraw to address 2, success - console.debug('Call to withdraw'); const txWithdrawal = await wallet.createAndSendNanoContractTransaction('withdraw', address2, { ncId: tx1.hash, actions: [ diff --git a/src/nano_contracts/methodArg.ts b/src/nano_contracts/methodArg.ts index e3079e610..dd0a7c591 100644 --- a/src/nano_contracts/methodArg.ts +++ b/src/nano_contracts/methodArg.ts @@ -14,6 +14,7 @@ import { BufferROExtract, NanoContractArgumentApiInputType, NanoContractArgumentApiInputSchema, + NanoContractArgumentByteTypes, } from './types'; import Serializer from './serializer'; import Deserializer from './deserializer'; @@ -52,9 +53,7 @@ function refineSingleValue( } else { return parse.data; } - } else if ( - ['bytes', 'BlueprintId', 'ContractId', 'TokenUid', 'TxOutputScript', 'VertexId'].includes(type) - ) { + } else if (NanoContractArgumentByteTypes.safeParse(type).success) { const parse = z .string() .regex(/^[0-9A-Fa-f]+$/) @@ -130,7 +129,7 @@ function refineSingleValue( /** * Type and value validation for non-container types. - * Returns the internal TS type for the argument given. + * Returns the internal parsed value for the argument given. */ const SingleValueApiInputScheme = z .tuple([ @@ -262,11 +261,7 @@ export class NanoContractMethodArgument { if (type === 'bool') { return (value as boolean) ? 'true' : 'false'; } - if ( - ['bytes', 'BlueprintId', 'ContractId', 'TokenUid', 'TxOutputScript', 'VertexId'].includes( - type - ) - ) { + if (NanoContractArgumentByteTypes.safeParse(type).success) { return (value as Buffer).toString('hex'); } if (type === 'VarInt') { diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 45b2ee41e..a19096cc6 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -59,22 +59,26 @@ export const NanoContractArgumentSchema = z.union([ ]); export type NanoContractArgumentType = z.output; +export const NanoContractArgumentByteTypes = z.enum([ + 'bytes', + 'Address', + 'BlueprintId', + 'ContractId', + 'TokenUid', + 'TxOutputScript', + 'VertexId', +]); + /** * Single type names */ export const NanoContractArgumentSingleTypeNameSchema = z.enum([ 'bool', - 'bytes', 'int', 'str', - 'Address', - 'BlueprintId', - 'ContractId', 'Timestamp', - 'TokenUid', - 'TxOutputScript', 'VarInt', - 'VertexId', + ...NanoContractArgumentByteTypes.options, ]); export type NanoContractArgumentSingleTypeName = z.output< typeof NanoContractArgumentSingleTypeNameSchema From 8f5290d20e2cd38cc210357f75d175dc2d768349 Mon Sep 17 00:00:00 2001 From: Andre Carneiro Date: Fri, 16 May 2025 16:33:18 -0300 Subject: [PATCH 22/23] feat: Address should not be on the bytes enum --- src/nano_contracts/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index a19096cc6..30ffe8fad 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -61,7 +61,6 @@ export type NanoContractArgumentType = z.output Date: Fri, 16 May 2025 17:17:00 -0300 Subject: [PATCH 23/23] chore: remove ncId from SignedData --- src/nano_contracts/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nano_contracts/types.ts b/src/nano_contracts/types.ts index 30ffe8fad..6125b4540 100644 --- a/src/nano_contracts/types.ts +++ b/src/nano_contracts/types.ts @@ -37,7 +37,6 @@ export const NanoContractSignedDataSchema = z.object({ type: z.string(), signature: z.instanceof(Buffer), value: NanoContractArgumentSingleSchema, - ncId: z.instanceof(Buffer).nullish(), }); export type NanoContractSignedData = z.output;