diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 46a5abf2..ef300be2 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "casper-client-sdk", - "version": "1.0.10", + "version": "1.0.11", "license": "Apache 2.0", "description": "SDK to interact with the Casper blockchain", "main": "dist/index.js", diff --git a/packages/sdk/src/lib/CLValue.ts b/packages/sdk/src/lib/CLValue.ts index 1e79695c..690b1a63 100644 --- a/packages/sdk/src/lib/CLValue.ts +++ b/packages/sdk/src/lib/CLValue.ts @@ -4,7 +4,6 @@ import { toBytesBytesArray, toBytesNumber, toBytesString, - toBytesStringList, toBytesU32, toBytesVecT } from './byterepr'; @@ -13,6 +12,8 @@ import { decodeBase16, encodeBase16 } from './Conversions'; import { Option } from './option'; import { byteHash } from './Contracts'; import { SignatureAlgorithm } from './Keys'; +import { jsonMember, jsonObject } from 'typedjson'; +import { ByteArray } from 'tweetnacl-ts'; const ED25519_PUBLIC_KEY_LENGTH = 32; const SECP256K1_PUBLIC_KEY_LENGTH = 33; @@ -168,12 +169,12 @@ export class Result { @staticImplements>() export class Bool extends CLTypedAndToBytes { - constructor(private b: boolean) { + constructor(public val: boolean) { super(); } public toBytes(): ByteArray { - return new Uint8Array([this.b ? 1 : 0]); + return new Uint8Array([this.val ? 1 : 0]); } public clType(): CLType { @@ -197,7 +198,7 @@ export class Bool extends CLTypedAndToBytes { abstract class NumberCoder extends CLTypedAndToBytes { public bitSize: number; public signed: boolean; - public value: BigNumberish; + public val: BigNumber; public name: string; protected constructor(bitSize: number, signed: boolean, value: BigNumberish) { @@ -205,11 +206,11 @@ abstract class NumberCoder extends CLTypedAndToBytes { this.name = (signed ? 'i' : 'u') + bitSize; this.bitSize = bitSize; this.signed = signed; - this.value = value; + this.val = BigNumber.from(value); } public toBytes = (): ByteArray => { - return toBytesNumber(this.bitSize, this.signed, this.value); + return toBytesNumber(this.bitSize, this.signed, this.val); }; public abstract clType(): CLType; @@ -330,20 +331,7 @@ export class U128 extends NumberCoder { } public static fromBytes(bytes: ByteArray): Result { - if (bytes.length < 1) { - return Result.Err(FromBytesError.EarlyEndOfStream); - } - const tmp = Uint8Array.from(bytes); - const n = tmp[0]; - if (n === 0 || n > 16) { - return Result.Err(FromBytesError.FormattingError); - } - if (n + 1 > bytes.length) { - return Result.Err(FromBytesError.EarlyEndOfStream); - } - const u128Bytes = tmp.subarray(1, 1 + n); - const rem = tmp.subarray(1 + n); - return Result.Ok(new U128(BigNumber.from(u128Bytes.reverse())), rem); + return fromBytesBigInt(bytes, 128); } } @@ -357,26 +345,13 @@ class U256 extends NumberCoder { return SimpleType.U256; } - public static fromBytes(bytes: ByteArray): Result { - if (bytes.length < 1) { - return Result.Err(FromBytesError.EarlyEndOfStream); - } - const tmp = Uint8Array.from(bytes); - const n = tmp[0]; - if (n === 0 || n > 32) { - return Result.Err(FromBytesError.FormattingError); - } - if (n + 1 > bytes.length) { - return Result.Err(FromBytesError.EarlyEndOfStream); - } - const u256Bytes = tmp.subarray(1, 1 + n); - const rem = tmp.subarray(1 + n); - return Result.Ok(new U256(BigNumber.from(u256Bytes.reverse())), rem); + public static fromBytes(bytes: ByteArray): Result { + return fromBytesBigInt(bytes, 256); } } @staticImplements>() -class U512 extends NumberCoder { +export class U512 extends NumberCoder { constructor(n: BigNumberish) { super(512, false, n); } @@ -386,20 +361,7 @@ class U512 extends NumberCoder { } public static fromBytes(bytes: ByteArray): Result { - if (bytes.length < 1) { - return Result.Err(FromBytesError.EarlyEndOfStream); - } - const tmp = Uint8Array.from(bytes); - const n = tmp[0]; - if (n === 0 || n > 64) { - return Result.Err(FromBytesError.FormattingError); - } - if (n + 1 > bytes.length) { - return Result.Err(FromBytesError.EarlyEndOfStream); - } - const u512Bytes = tmp.subarray(1, 1 + n); - const rem = tmp.subarray(1 + n); - return Result.Ok(new U512(BigNumber.from(u512Bytes.reverse())), rem); + return fromBytesBigInt(bytes, 512); } } @@ -420,12 +382,12 @@ export class Unit extends CLTypedAndToBytes { @staticImplements>() export class StringValue extends CLTypedAndToBytes { - constructor(public str: string) { + constructor(public val: string) { super(); } public toBytes = () => { - return toBytesString(this.str); + return toBytesString(this.val); }; public clType(): CLType { @@ -437,7 +399,7 @@ export class StringValue extends CLTypedAndToBytes { if (res.hasError()) { return Result.Err(res.error); } - const len = res.value.value as number; + const len = res.value.val.toNumber(); const str = Buffer.from(res.remainder.subarray(0, len)).toString('utf8'); return Result.Ok( new StringValue(str), @@ -552,7 +514,7 @@ export class List extends CLTypedAndToBytes { if (u32Res.hasError()) { return Result.Err(u32Res.error); } - const size = u32Res.value.value as number; + const size = u32Res.value.val.toNumber(); const vec = []; let remainder = u32Res.remainder; for (let i = 0; i < size; i++) { @@ -814,7 +776,7 @@ export class MapValue extends CLTypedAndToBytes { if (u32Res.hasError()) { return Result.Err(u32Res.error); } - const size = u32Res.value.value as number; + const size = u32Res.value.val.toNumber(); const vec: MapEntry[] = []; let remainder = u32Res.remainder; for (let i = 0; i < size; i++) { @@ -834,49 +796,169 @@ export class MapValue extends CLTypedAndToBytes { } } -export class OptionType { +@staticImplements>() +class ByteArrayValue extends CLTypedAndToBytes { + constructor(public rawBytes: ByteArray) { + super(); + } + + public clType(): CLType { + return CLTypeHelper.byteArray(this.rawBytes.length); + } + + public toBytes(): ByteArray { + return toBytesBytesArray(this.rawBytes); + } + + public static fromBytes(bytes: ByteArray): Result { + const b = new ByteArrayValue(bytes); + return Result.Ok(b, bytes.subarray(32)); + } +} + +const fromBytesBigInt: ( + bytes: ByteArray, + bitSize: number +) => Result = (bytes: ByteArray, bitSize: number) => { + const byteSize = bitSize / 8; + if (bytes.length < 1) { + return Result.Err(FromBytesError.EarlyEndOfStream); + } + const tmp = Uint8Array.from(bytes); + const n = tmp[0]; + if (n > byteSize) { + return Result.Err(FromBytesError.FormattingError); + } + if (n + 1 > bytes.length) { + return Result.Err(FromBytesError.EarlyEndOfStream); + } + let bigIntBytes; + if (n === 0) { + bigIntBytes = [0]; + } else { + bigIntBytes = tmp.subarray(1, 1 + n); + } + const rem = tmp.subarray(1 + n); + if (bitSize === 128) { + return Result.Ok(new U128(BigNumber.from(bigIntBytes.reverse())), rem); + } else if (bitSize === 256) { + return Result.Ok(new U256(BigNumber.from(bigIntBytes.reverse())), rem); + } else if (bitSize === 512) { + return Result.Ok(new U512(BigNumber.from(bigIntBytes.reverse())), rem); + } else { + return Result.Err(FromBytesError.FormattingError); + } +}; + +export interface ToJSON { + toJSON: () => any; +} + +export class OptionType implements ToJSON { + public static TypeId = 'Option'; public tag = ComplexType.Option; constructor(public innerType: CLType) {} + + public toJSON(): any { + const innerTypeInJSON = clTypeToJSON(this.innerType); + return { + [OptionType.TypeId]: innerTypeInJSON + }; + } } -class ListType { +class ListType implements ToJSON { + public static TypeId = 'List'; public tag = ComplexType.List; public innerType: CLType; constructor(innerType: CLType) { this.innerType = innerType; } + + public toJSON(): any { + const innerTypeInJSON = clTypeToJSON(this.innerType); + return { + [ListType.TypeId]: innerTypeInJSON + }; + } } -class ByteArrayType { +class ByteArrayType implements ToJSON { + public static TypeId = 'ByteArray'; public tag = ComplexType.ByteArray; constructor(public size: number) {} + + public toJSON() { + return { + [ByteArrayType.TypeId]: this.size + }; + } } -class MapType { +class MapType implements ToJSON { + public static TypeId = 'Map'; public tag = ComplexType.Map; constructor(public keyType: CLType, public valueType: CLType) {} + + public toJSON(): any { + return { + [MapType.TypeId]: { + key: clTypeToJSON(this.keyType), + value: clTypeToJSON(this.valueType) + } + }; + } } -class Tuple1Type { +class Tuple1Type implements ToJSON { + public static TypeId = 'Tuple1'; public tag = ComplexType.Tuple1; constructor(public t0: CLType) {} + + public toJSON(): any { + const t0TypeInJSON = clTypeToJSON(this.t0); + return { + [Tuple1Type.TypeId]: t0TypeInJSON + }; + } } -class Tuple2Type { +class Tuple2Type implements ToJSON { + public static TypeId = 'Tuple2'; public tag = ComplexType.Tuple2; constructor(public t0: CLType, public t1: CLType) {} + + public toJSON(): any { + const t0TypeInJSON = clTypeToJSON(this.t0); + const t1TypeInJSON = clTypeToJSON(this.t1); + return { + [Tuple2Type.TypeId]: [t0TypeInJSON, t1TypeInJSON] + }; + } } class Tuple3Type { + public static TypeId = 'Tuple3'; + public tag = ComplexType.Tuple3; constructor(public t0: CLType, public t1: CLType, public t2: CLType) {} + + public toJSON(): any { + const t0TypeInJSON = clTypeToJSON(this.t0); + const t1TypeInJSON = clTypeToJSON(this.t1); + const t2TypeInJSON = clTypeToJSON(this.t2); + + return { + [Tuple3Type.TypeId]: [t0TypeInJSON, t1TypeInJSON, t2TypeInJSON] + }; + } } export type CLType = @@ -1026,7 +1108,7 @@ export class CLTypeHelper { return Result.Err(sizeRes.error); } return Result.Ok( - CLTypeHelper.byteArray(sizeRes.value.value as number), + CLTypeHelper.byteArray(sizeRes.value.val.toNumber()), sizeRes.remainder ); } @@ -1107,7 +1189,6 @@ export class CLTypeHelper { case ComplexType.Any: // todo(abner) support Any throw new Error('Any type is unsupported now'); - break; default: return Result.Err(FromBytesError.FormattingError); } @@ -1174,35 +1255,6 @@ export class CLTypeHelper { } } -@staticImplements>() -class ByteArrayValue extends CLTypedAndToBytes { - constructor(public rawBytes: ByteArray) { - super(); - } - - public clType(): CLType { - return CLTypeHelper.byteArray(this.rawBytes.length); - } - - public toBytes(): ByteArray { - return toBytesBytesArray(this.rawBytes); - } - - public static fromBytes(bytes: ByteArray): Result { - const u32Res = U32.fromBytes(bytes); - if (u32Res.hasError()) { - return Result.Err(u32Res.error); - } - const size = u32Res.value.value as number; - if (u32Res.remainder.length < size) { - return Result.Err(FromBytesError.EarlyEndOfStream); - } - const b = new ByteArrayValue(u32Res.remainder.subarray(0, length)); - const rem = u32Res.remainder.subarray(length); - return Result.Ok(b, rem); - } -} - export class CLTypedAndToBytesHelper { public static bool = (b: boolean) => { return new Bool(b); @@ -1286,20 +1338,176 @@ export class CLTypedAndToBytesHelper { } } +function toJSONSimpleType(type: SimpleType) { + switch (type) { + case SimpleType.Bool: + return 'Bool'; + case SimpleType.I32: + return 'I32'; + case SimpleType.I64: + return 'I64'; + case SimpleType.U8: + return 'U8'; + case SimpleType.U32: + return 'U32'; + case SimpleType.U64: + return 'U64'; + case SimpleType.U128: + return 'U128'; + case SimpleType.U256: + return 'U256'; + case SimpleType.U512: + return 'U512'; + case SimpleType.Unit: + return 'Unit'; + case SimpleType.String: + return 'String'; + case SimpleType.Key: + return 'Key'; + case SimpleType.URef: + return 'URef'; + case SimpleType.PublicKey: + return 'PublicKey'; + } +} + +function jsonToSimpleType(str: string): CLType { + switch (str) { + case 'Bool': + return SimpleType.Bool; + case 'I32': + return SimpleType.I32; + case 'I64': + return SimpleType.I64; + case 'U8': + return SimpleType.U8; + case 'U32': + return SimpleType.U32; + case 'U64': + return SimpleType.U64; + case 'U128': + return SimpleType.U128; + case 'U256': + return SimpleType.U256; + case 'U512': + return SimpleType.U512; + case 'Unit': + return SimpleType.Unit; + case 'String': + return SimpleType.String; + case 'Key': + return SimpleType.Key; + case 'URef': + return SimpleType.URef; + case 'PublicKey': + return SimpleType.PublicKey; + default: + throw new Error(`The type ${str} is not supported`); + } +} + +const clTypeToJSON = (type: CLType) => { + if ( + type instanceof ListType || + type instanceof Tuple1Type || + type instanceof Tuple2Type || + type instanceof Tuple3Type || + type instanceof ByteArrayType || + type instanceof MapType || + type instanceof OptionType + ) { + return type.toJSON(); + } else { + return toJSONSimpleType(type); + } +}; + +const jsonToCLType = (json: any): CLType => { + if (typeof json === typeof 'str') { + return jsonToSimpleType(json); + } else if (typeof json === typeof {}) { + if (ListType.TypeId in json) { + const innerType = jsonToCLType(json[ListType.TypeId]); + return CLTypeHelper.list(innerType); + } else if (Tuple1Type.TypeId in json) { + const t0Type = jsonToCLType(json[Tuple1Type.TypeId][0]); + return CLTypeHelper.tuple1(t0Type); + } else if (Tuple2Type.TypeId in json) { + const innerTypes = json[Tuple2Type.TypeId]; + const t0Type = jsonToCLType(innerTypes[0]); + const t1Type = jsonToCLType(innerTypes[1]); + return CLTypeHelper.tuple2(t0Type, t1Type); + } else if (Tuple3Type.TypeId in json) { + const innerTypes = json[Tuple2Type.TypeId]; + const t0Type = jsonToCLType(innerTypes[0]); + const t1Type = jsonToCLType(innerTypes[1]); + const t2Type = jsonToCLType(innerTypes[2]); + return CLTypeHelper.tuple3(t0Type, t1Type, t2Type); + } else if (ByteArrayType.TypeId in json) { + const size = json[ByteArrayType.TypeId]; + return CLTypeHelper.byteArray(size); + } else if (OptionType.TypeId in json) { + const innerType = jsonToCLType(json[OptionType.TypeId]); + return CLTypeHelper.option(innerType); + } else if (MapType.TypeId in json) { + const keyType = jsonToCLType(json[MapType.TypeId].key); + const valueType = jsonToCLType(json[MapType.TypeId].value); + return CLTypeHelper.map(keyType, valueType); + } else { + throw new Error(`The type ${json} is not supported`); + } + } else { + throw new Error(`The type ${json} is not supported`); + } +}; + +function deserializeCLValue(_a: any, _b: any) { + const v = fromBytesByCLType(_a.clType, decodeBase16(_a.bytes)); + const ret = CLValue.fromT(v.value); + return ret; +} + /** * A Casper value, i.e. a value which can be stored and manipulated by smart contracts. * * It holds the underlying data as a type-erased, serialized array of bytes and also holds the * [[CLType]] of the underlying data as a separate member. */ +@jsonObject({ + initializer: (a, b) => deserializeCLValue(a, b) +}) export class CLValue implements ToBytes { + @jsonMember({ + name: 'cl_type', + serializer: clTypeToJSON, + deserializer: jsonToCLType + }) + public clType: CLType; + + @jsonMember({ + constructor: String + }) + public bytes: string; + + public parsedToJson: any; + + private value: CLTypedAndToBytes; + /** - * Please use static methods to constructs a new `CLValue` + * Please use static methodsto constructs a new `CLValue` */ - private constructor(private bytes: ByteArray, private clType: CLType) {} + private constructor(value: CLTypedAndToBytes, clType: CLType) { + this.value = value; + this.clType = clType; + this.bytes = encodeBase16(this.value.toBytes()); + } + + public get clValueBytes() { + return this.value.toBytes(); + } public static fromT(v: T) { - return new CLValue(v.toBytes(), v.clType()); + return new CLValue(v, v.clType()); } /** @@ -1307,7 +1515,7 @@ export class CLValue implements ToBytes { */ public toBytes() { return concat([ - toBytesArrayU8(this.bytes), + toBytesArrayU8(this.clValueBytes), CLTypeHelper.toBytesHelper(this.clType) ]); } @@ -1321,10 +1529,19 @@ export class CLValue implements ToBytes { if (clTypeRes.hasError()) { return Result.Err(clTypeRes.error); } - const clValue = new CLValue(bytesRes.value.rawBytes, clTypeRes.value); + const v = fromBytesByCLType(clTypeRes.value, bytesRes.value.rawBytes); + const clValue = new CLValue(v.value, clTypeRes.value); return Result.Ok(clValue, clTypeRes.remainder); } + protected reconstruct() { + const v = fromBytesByCLType(this.clType, decodeBase16(this.bytes)); + if (v.hasError()) { + throw new Error('Failed to deserialize CLValue'); + } + this.value = v.value; + } + public static bool = (b: boolean) => { return CLValue.fromT(new Bool(b)); }; @@ -1378,10 +1595,10 @@ export class CLValue implements ToBytes { }; public static stringList = (strings: string[]) => { - return new CLValue( - toBytesStringList(strings), - CLTypeHelper.list(SimpleType.String) + const v = CLTypedAndToBytesHelper.list( + strings.map(s => CLTypedAndToBytesHelper.string(s)) ); + return new CLValue(v, CLTypeHelper.list(SimpleType.String)); }; public static list(vec: T[]) { @@ -1415,6 +1632,105 @@ export class CLValue implements ToBytes { public static byteArray(bytes: ByteArray) { return CLValue.fromT(new ByteArrayValue(bytes)); } + + public isBigNumber() { + return ( + this.clType === SimpleType.U8 || + this.clType === SimpleType.I32 || + this.clType === SimpleType.I64 || + this.clType === SimpleType.U32 || + this.clType === SimpleType.U64 || + this.clType === SimpleType.U128 || + this.clType === SimpleType.U256 || + this.clType === SimpleType.U512 + ); + } + + public asBigNumber(): BigNumber { + if (this.isBigNumber()) { + const numberCoder = this.value as NumberCoder; + return BigNumber.from(numberCoder.val); + } else { + throw new Error("The CLValue can't convert to BigNumber"); + } + } + + public isBoolean() { + return this.clType === SimpleType.Bool; + } + + public asBoolean() { + if (!this.isBoolean()) { + throw new Error("The CLValue can't convert to Boolean"); + } + return (this.value as Bool).val; + } + + public isString() { + return this.clType === SimpleType.String; + } + + public asString() { + if (!this.isString()) { + throw new Error("The CLValue can't convert to String"); + } + return (this.value as StringValue).val; + } + + public isPublicKey() { + return this.clType === SimpleType.PublicKey; + } + + public asPublicKey(): PublicKey { + if (!this.isPublicKey()) { + throw new Error("The CLValue can't convert to PublicKey"); + } + return this.value as PublicKey; + } + + public isKey() { + return this.clType === SimpleType.Key; + } + + public asKey() { + if (!this.isKey()) { + throw new Error("The CLValue can't convert to Key"); + } + return this.value as KeyValue; + } + + public isURef() { + return this.clType === SimpleType.URef; + } + + public asURef() { + if (!this.isURef()) { + throw new Error("The CLValue can't convert to URef"); + } + return this.value as URef; + } + + public isBytesArray() { + return this.clType instanceof ByteArrayType; + } + + public asBytesArray() { + if (!this.isBytesArray()) { + throw new Error("The CLValue can't convert to BytesArray"); + } + return (this.value as ByteArrayValue).toBytes(); + } + + public isOption() { + return this.clType instanceof OptionType; + } + + public asOption() { + if (!this.isOption()) { + throw new Error("The CLValue can't convert to Option"); + } + return this.value as Option; + } } export enum KeyVariant { @@ -1481,6 +1797,18 @@ export class KeyValue extends CLTypedAndToBytes { public uRef: URef | null; public account: AccountHash | null; + public isHash() { + return this.variant === KeyVariant.HASH_ID; + } + + public isURef() { + return this.variant === KeyVariant.UREF_ID; + } + + public isAccount() { + return this.variant === KeyVariant.ACCOUNT_ID; + } + /** Creates a `Key` from a given [[URef]]. */ public static fromURef(uref: URef): KeyValue { const key = new KeyValue(); @@ -1621,6 +1949,14 @@ export class URef extends CLTypedAndToBytes { return new URef(addr, accessRight); } + public toFormattedStr() { + return [ + FORMATTED_STRING_PREFIX, + encodeBase16(this.uRefAddr), + this.accessRights.toString(8) + ].join('-'); + } + /** * Serializes the URef into an array of bytes that represents it in the Casper serialization * format. diff --git a/packages/sdk/src/lib/CasperClient.ts b/packages/sdk/src/lib/CasperClient.ts index eb715da3..2c8db8fc 100644 --- a/packages/sdk/src/lib/CasperClient.ts +++ b/packages/sdk/src/lib/CasperClient.ts @@ -1,18 +1,7 @@ -import { - AccountDeploy, - CasperServiceByJsonRPC, - DeployResult, - EventService, - TransferResult -} from '../services'; +import { AccountDeploy, CasperServiceByJsonRPC, DeployResult, EventService, TransferResult } from '../services'; import { DeployUtil, Keys, PublicKey } from './index'; import { encodeBase16 } from './Conversions'; -import { - Deploy, - DeployParams, - ExecutableDeployItem, - Transfer -} from './DeployUtil'; +import { Deploy, DeployParams, ExecutableDeployItem } from './DeployUtil'; import { AsymmetricKey, SignatureAlgorithm } from './Keys'; import { CasperHDKey } from './CasperHDKey'; @@ -165,6 +154,15 @@ export class CasperClient { return DeployUtil.deployToJson(deploy); } + /** + * Convert the json to deploy object + * + * @param json + */ + public deployFromJson(json: any) { + return DeployUtil.deployFromJson(json); + }; + /** * Construct the deploy for transfer purpose * @@ -174,9 +172,12 @@ export class CasperClient { */ public makeTransferDeploy( deployParams: DeployParams, - session: Transfer, + session: ExecutableDeployItem, payment: ExecutableDeployItem ): Deploy { + if (!session.isTransfer()) { + throw new Error('The session is not a Transfer ExecutableDeployItem'); + } return this.makeDeploy(deployParams, session, payment); } diff --git a/packages/sdk/src/lib/Contracts.ts b/packages/sdk/src/lib/Contracts.ts index 03774289..c142f7de 100644 --- a/packages/sdk/src/lib/Contracts.ts +++ b/packages/sdk/src/lib/Contracts.ts @@ -2,10 +2,10 @@ import blake from 'blakejs'; import * as fs from 'fs'; import { PublicKey } from '../index'; import * as DeployUtil from './DeployUtil'; +import { DeployParams, ExecutableDeployItem } from './DeployUtil'; import { RuntimeArgs } from './RuntimeArgs'; -import { CLValue, AccountHash, KeyValue } from './CLValue'; +import { AccountHash, CLValue, KeyValue } from './CLValue'; import { AsymmetricKey } from './Keys'; -import { DeployParams } from './DeployUtil'; // https://www.npmjs.com/package/tweetnacl-ts // https://github.com/dcposch/blakejs @@ -53,18 +53,12 @@ export class Contract { signingKeyPair: AsymmetricKey, chainName: string ): DeployUtil.Deploy { - const session = new DeployUtil.ModuleBytes( - this.sessionWasm, - args.toBytes() - ); + const session = ExecutableDeployItem.newModuleBytes(this.sessionWasm, args); const paymentArgs = RuntimeArgs.fromMap({ amount: CLValue.u512(paymentAmount.toString()) }); - const payment = new DeployUtil.ModuleBytes( - this.paymentWasm, - paymentArgs.toBytes() - ); + const payment = ExecutableDeployItem.newModuleBytes(this.paymentWasm, paymentArgs); const deploy = DeployUtil.makeDeploy( new DeployParams(accountPublicKey, chainName), @@ -82,7 +76,8 @@ export class BoundContract { constructor( private contract: Contract, private contractKeyPair: AsymmetricKey - ) {} + ) { + } public deploy( args: RuntimeArgs, diff --git a/packages/sdk/src/lib/DeployUtil.ts b/packages/sdk/src/lib/DeployUtil.ts index 5ff676c8..beb458eb 100644 --- a/packages/sdk/src/lib/DeployUtil.ts +++ b/packages/sdk/src/lib/DeployUtil.ts @@ -6,7 +6,7 @@ import { concat } from '@ethersproject/bytes'; import blake from 'blakejs'; import { Option } from './option'; -import { encodeBase16 } from './Conversions'; +import { decodeBase16, encodeBase16 } from './Conversions'; import humanizeDuration from 'humanize-duration'; import { CLTypedAndToBytesHelper, @@ -25,10 +25,12 @@ import { toBytesVecT } from './byterepr'; import { RuntimeArgs } from './RuntimeArgs'; -import JSBI from 'jsbi'; +// import JSBI from 'jsbi'; import { Keys, URef } from './index'; import { AsymmetricKey, SignatureAlgorithm } from './Keys'; import { BigNumberish } from '@ethersproject/bignumber'; +import { jsonArrayMember, jsonMember, jsonObject, TypedJSON } from 'typedjson'; +import { ByteArray } from 'tweetnacl-ts'; const shortEnglishHumanizer = humanizeDuration.humanizer({ spacer: '', @@ -49,6 +51,16 @@ const shortEnglishHumanizer = humanizeDuration.humanizer({ } }); +const byteArrayJsonSerializer: (bytes: ByteArray) => string = ( + bytes: ByteArray +) => { + return encodeBase16(bytes); +}; + +const byteArrayJsonDeserializer: (str: string) => ByteArray = (str: string) => { + return decodeBase16(str); +}; + /** * Return a humanizer duration * @param ttl in milliseconds @@ -57,38 +69,85 @@ export const humanizerTTL = (ttl: number) => { return shortEnglishHumanizer(ttl); }; -/** - * The header portion of a Deploy - */ -export interface DeployHeader { - /** - * The account within which the deploy will be run. - */ - account: PublicKey; - /** - * When the deploy was created. - */ - timestamp: number; - /** - * How long the deploy will stay valid. - */ - ttl: number; - /** - * Price per gas unit for this deploy. - */ - gasPrice: number; - /** - * Hash of the Wasm code. - */ - bodyHash: ByteArray; - /** - * Other deploys that have to be run before this one. - */ - dependencies: ByteArray[]; +@jsonObject +export class DeployHeader implements ToBytes { + @jsonMember({ + serializer: (account: PublicKey) => { + return account.toAccountHex(); + }, + deserializer: (hexStr: string) => { + return PublicKey.fromHex(hexStr); + } + }) + public account: PublicKey; + + @jsonMember({ constructor: Number }) + public timestamp: number; + + @jsonMember({ constructor: Number }) + public ttl: number; + + @jsonMember({ constructor: Number, name: 'gas_price' }) + public gasPrice: number; + + @jsonMember({ + name: 'body_hash', + serializer: byteArrayJsonSerializer, + deserializer: byteArrayJsonDeserializer + }) + public bodyHash: ByteArray; + + @jsonArrayMember(ByteArray, { + serializer: (value: ByteArray[]) => + value.map(it => byteArrayJsonSerializer(it)), + deserializer: (json: any) => + json.map((it: string) => byteArrayJsonDeserializer(it)) + }) + public dependencies: ByteArray[]; + + @jsonMember({ name: 'chain_name', constructor: String }) + public chainName: string; + /** - * Which chain the deploy is supposed to be run on. + * The header portion of a Deploy + * + * @param account The account within which the deploy will be run. + * @param timestamp When the deploy was created. + * @param ttl How long the deploy will stay valid. + * @param gasPrice Price per gas unit for this deploy. + * @param bodyHash Hash of the Wasm code. + * @param dependencies Other deploys that have to be run before this one. + * @param chainName Which chain the deploy is supposed to be run on. */ - chainName: string; + constructor( + account: PublicKey, + timestamp: number, + ttl: number, + gasPrice: number, + bodyHash: ByteArray, + dependencies: ByteArray[], + chainName: string + ) { + this.account = account; + this.timestamp = timestamp; + this.ttl = ttl; + this.gasPrice = gasPrice; + this.bodyHash = bodyHash; + this.dependencies = dependencies; + this.chainName = chainName; + } + + public toBytes(): ByteArray { + return concat([ + this.account.toBytes(), + toBytesU64(this.timestamp), + toBytesU64(this.ttl), + toBytesU64(this.gasPrice), + toBytesDeployHash(this.bodyHash), + toBytesVecT(this.dependencies.map(d => new DeployHash(d))), + toBytesString(this.chainName) + ]); + } } /** @@ -102,102 +161,96 @@ class DeployHash implements ToBytes { } } -/** - * Serialized DeployHeader to an array of bytes - * @param deployHeader - */ -const toBytesDeployHeader = (deployHeader: DeployHeader) => { - return concat([ - deployHeader.account.toBytes(), - toBytesU64(deployHeader.timestamp), - toBytesU64(deployHeader.ttl), - toBytesU64(deployHeader.gasPrice), - toBytesDeployHash(deployHeader.bodyHash), - toBytesVecT(deployHeader.dependencies.map(d => new DeployHash(d))), - toBytesString(deployHeader.chainName) - ]); -}; - -/** - * A deploy containing a smart contract along with the requester's signature(s). - */ -export interface Deploy { - /** - * The DeployHash identifying this Deploy - */ - hash: ByteArray; - /** - * The deployHeader - */ +export interface DeployJson { + session: Record; + approvals: { signature: string; signer: string }[]; header: DeployHeader; - /** - * The ExecutableDeployItem for payment code. - */ - payment: ExecutableDeployItem; - /** - * the ExecutableDeployItem for session code. - */ - session: ExecutableDeployItem; - /** - * An array of signature and public key of the signers, who approve this deploy - */ - approvals: Approval[]; + payment: Record; + hash: string; } /** * A struct containing a signature and the public key of the signer. */ +@jsonObject export class Approval { + @jsonMember({ constructor: String }) public signer: string; + @jsonMember({ constructor: String }) public signature: string; } -interface ToJson { - toJson(): Record; -} - -export abstract class ExecutableDeployItem implements ToBytes, ToJson { +abstract class ExecutableDeployItemInternal implements ToBytes { public abstract tag: number; + public abstract args: RuntimeArgs; + public abstract toBytes(): ByteArray; - public abstract toJson(): Record; + public getArgByName(argName: string): CLValue | undefined { + return this.args.args.get(argName); + } } -export class ModuleBytes extends ExecutableDeployItem { +@jsonObject +export class ModuleBytes extends ExecutableDeployItemInternal { public tag = 0; - constructor(private moduleBytes: Uint8Array, private args: Uint8Array) { + @jsonMember({ + name: 'module_bytes', + serializer: byteArrayJsonSerializer, + deserializer: byteArrayJsonDeserializer + }) + public moduleBytes: Uint8Array; + + @jsonMember({ + constructor: RuntimeArgs + }) + public args: RuntimeArgs; + + constructor(moduleBytes: Uint8Array, args: RuntimeArgs) { super(); + + this.moduleBytes = moduleBytes; + this.args = args; } public toBytes(): ByteArray { return concat([ Uint8Array.from([this.tag]), toBytesArrayU8(this.moduleBytes), - toBytesArrayU8(this.args) + toBytesArrayU8(this.args.toBytes()) ]); } - - public toJson(): Record { - return { - ModuleBytes: { - module_bytes: encodeBase16(this.moduleBytes), - args: encodeBase16(this.args) - } - }; - } } -export class StoredContractByHash extends ExecutableDeployItem { +@jsonObject +export class StoredContractByHash extends ExecutableDeployItemInternal { public tag = 1; - constructor( - private hash: Uint8Array, - private entryPoint: string, - private args: Uint8Array - ) { + @jsonMember({ + serializer: byteArrayJsonSerializer, + deserializer: byteArrayJsonDeserializer + }) + public hash: ByteArray; + + @jsonMember({ + name: 'entry_point', + constructor: String + }) + public entryPoint: string; + + @jsonMember({ + constructor: RuntimeArgs + }) + public args: RuntimeArgs; + + constructor(hash: ByteArray, entryPoint: string, args: RuntimeArgs) { super(); + + this.entryPoint = entryPoint; + this.args = args; + this.hash = hash; } public toBytes(): ByteArray { @@ -205,30 +258,35 @@ export class StoredContractByHash extends ExecutableDeployItem { Uint8Array.from([this.tag]), toBytesBytesArray(this.hash), toBytesString(this.entryPoint), - toBytesArrayU8(this.args) + toBytesArrayU8(this.args.toBytes()) ]); } - - public toJson(): Record { - return { - StoredContractByHash: { - hash: encodeBase16(this.hash), - entry_point: this.entryPoint, - args: encodeBase16(this.args) - } - }; - } } -export class StoredContractByName extends ExecutableDeployItem { +@jsonObject +export class StoredContractByName extends ExecutableDeployItemInternal { public tag = 2; - constructor( - private name: string, - private entryPoint: string, - private args: Uint8Array - ) { + @jsonMember({ constructor: String }) + public name: string; + + @jsonMember({ + name: 'entry_point', + constructor: String + }) + public entryPoint: string; + + @jsonMember({ + constructor: RuntimeArgs + }) + public args: RuntimeArgs; + + constructor(name: string, entryPoint: string, args: RuntimeArgs) { super(); + + this.name = name; + this.entryPoint = entryPoint; + this.args = args; } public toBytes(): ByteArray { @@ -236,31 +294,40 @@ export class StoredContractByName extends ExecutableDeployItem { Uint8Array.from([this.tag]), toBytesString(this.name), toBytesString(this.entryPoint), - toBytesArrayU8(this.args) + toBytesArrayU8(this.args.toBytes()) ]); } - - public toJson(): Record { - return { - StoredContractByName: { - name: this.name, - entry_point: this.entryPoint, - args: encodeBase16(this.args) - } - }; - } } -export class StoredVersionedContractByName extends ExecutableDeployItem { +@jsonObject +export class StoredVersionedContractByName extends ExecutableDeployItemInternal { public tag = 4; + @jsonMember({ constructor: String }) + public name: string; + + @jsonMember({ constructor: Number, preserveNull: true }) + public version: number | null; + + @jsonMember({ name: 'entry_point', constructor: String }) + public entryPoint: string; + + @jsonMember({ + constructor: RuntimeArgs + }) + public args: RuntimeArgs; + constructor( - private name: string, - private version: number | null, - private entryPoint: string, - private args: Uint8Array + name: string, + version: number | null, + entryPoint: string, + args: RuntimeArgs ) { super(); + this.name = name; + this.version = version; + this.entryPoint = entryPoint; + this.args = args; } public toBytes(): ByteArray { @@ -275,30 +342,54 @@ export class StoredVersionedContractByName extends ExecutableDeployItem { toBytesString(this.name), serializedVersion.toBytes(), toBytesString(this.entryPoint), - toBytesArrayU8(this.args) + toBytesArrayU8(this.args.toBytes()) ]); } - - public toJson(): Record { - return { - StoredVersionedContractByName: { - name: this.name, - entry_point: this.entryPoint, - args: encodeBase16(this.args) - } - }; - } } -export class StoredVersionedContractByHash extends ExecutableDeployItem { +@jsonObject +export class StoredVersionedContractByHash extends ExecutableDeployItemInternal { + public tag = 3; + + @jsonMember({ + serializer: byteArrayJsonSerializer, + deserializer: byteArrayJsonDeserializer + }) public hash: Uint8Array; + + @jsonMember({ + constructor: Number, + preserveNull: true + }) public version: number | null; + + @jsonMember({ + name: 'entry_point', + constructor: String + }) public entryPoint: string; - public args: ByteArray; - public tag = 3; + + @jsonMember({ + constructor: RuntimeArgs + }) + public args: RuntimeArgs; + + constructor( + hash: Uint8Array, + version: number | null, + entryPoint: string, + args: RuntimeArgs + ) { + super(); + this.hash = hash; + this.version = version; + this.entryPoint = entryPoint; + this.args = args; + } public toBytes(): ByteArray { let serializedVersion; + if (this.version === null) { serializedVersion = new Option(null, CLTypeHelper.u32()); } else { @@ -309,25 +400,189 @@ export class StoredVersionedContractByHash extends ExecutableDeployItem { toBytesBytesArray(this.hash), serializedVersion.toBytes(), toBytesString(this.entryPoint), - toBytesArrayU8(this.args) + toBytesArrayU8(this.args.toBytes()) ]); } +} + +@jsonObject +export class Transfer extends ExecutableDeployItemInternal { + public tag = 5; + + @jsonMember({ + constructor: RuntimeArgs + }) + public args: RuntimeArgs; - public toJson(): Record { - return { - StoredVersionedContractByHash: { - hash: encodeBase16(this.hash), - version: this.version, - entry_point: this.entryPoint, - args: encodeBase16(this.args) - } - }; + /** + * Constructor for Transfer deploy item. + * @param amount The number of motes to transfer + * @param target URef of the target purse or the public key of target account. You could generate this public key from accountHex by PublicKey.fromHex + * @param sourcePurse URef of the source purse. If this is omitted, the main purse of the account creating this \ + * transfer will be used as the source purse + * @param id user-defined transfer id + */ + constructor(args: RuntimeArgs) { + super(); + this.args = args; + } + + public toBytes(): ByteArray { + return concat([ + Uint8Array.from([this.tag]), + toBytesArrayU8(this.args.toBytes()) + ]); } } -export class Transfer extends ExecutableDeployItem { - public args: ByteArray; - public tag = 5; +@jsonObject +export class ExecutableDeployItem implements ToBytes { + @jsonMember({ + name: 'ModuleBytes', + constructor: ModuleBytes + }) + public moduleBytes?: ModuleBytes; + + @jsonMember({ + name: 'StoredVersionedContractByHash', + constructor: StoredContractByHash + }) + public storedContractByHash?: StoredContractByHash; + + @jsonMember({ + name: 'StoredContractByName', + constructor: StoredContractByName + }) + public storedContractByName?: StoredContractByName; + + @jsonMember({ + name: 'StoredVersionedContractByHash', + constructor: StoredVersionedContractByHash + }) + public storedVersionedContractByHash?: StoredVersionedContractByHash; + + @jsonMember({ + name: 'StoredVersionedContractByName', + constructor: StoredVersionedContractByName + }) + public storedVersionedContractByName?: StoredVersionedContractByName; + @jsonMember({ + name: 'Transfer', + constructor: Transfer + }) + public transfer?: Transfer; + + public toBytes(): ByteArray { + if (this.isModuleBytes()) { + return this.moduleBytes!.toBytes(); + } else if (this.isStoredContractByHash()) { + return this.storedContractByHash!.toBytes(); + } else if (this.isStoredContractByName()) { + return this.storedContractByName!.toBytes(); + } else if (this.isStoredVersionContractByHash()) { + return this.storedVersionedContractByHash!.toBytes(); + } else if (this.isStoredVersionContractByName()) { + return this.storedVersionedContractByName!.toBytes(); + } else if (this.isTransfer()) { + return this.transfer!.toBytes(); + } + throw new Error('failed to serialize ExecutableDeployItemJsonWrapper'); + } + + public getArgByName(name: string): CLValue | undefined { + if (this.isModuleBytes()) { + return this.moduleBytes!.getArgByName(name); + } else if (this.isStoredContractByHash()) { + return this.storedContractByHash!.getArgByName(name); + } else if (this.isStoredContractByName()) { + return this.storedContractByName!.getArgByName(name); + } else if (this.isStoredVersionContractByHash()) { + return this.storedVersionedContractByHash!.getArgByName(name); + } else if (this.isStoredVersionContractByName()) { + return this.storedVersionedContractByName!.getArgByName(name); + } else if (this.isTransfer()) { + return this.transfer!.getArgByName(name); + } + throw new Error('failed to serialize ExecutableDeployItemJsonWrapper'); + } + + public static fromExecutableDeployItemInternal( + item: ExecutableDeployItemInternal + ) { + const res = new ExecutableDeployItem(); + switch (item.tag) { + case 0: + res.moduleBytes = item as ModuleBytes; + break; + case 1: + res.storedContractByHash = item as StoredContractByHash; + break; + case 2: + res.storedContractByName = item as StoredContractByName; + break; + case 3: + res.storedVersionedContractByHash = item as StoredVersionedContractByHash; + break; + case 4: + res.storedVersionedContractByName = item as StoredVersionedContractByName; + break; + case 5: + res.transfer = item as Transfer; + break; + } + return res; + } + + public static newModuleBytes( + moduleBytes: ByteArray, + args: RuntimeArgs + ): ExecutableDeployItem { + return ExecutableDeployItem.fromExecutableDeployItemInternal( + new ModuleBytes(moduleBytes, args) + ); + } + + public static newStoredContractByHash( + hash: Uint8Array, + entryPoint: string, + args: RuntimeArgs + ) { + return ExecutableDeployItem.fromExecutableDeployItemInternal( + new StoredContractByHash(hash, entryPoint, args) + ); + } + + public static newStoredContractByName( + name: string, + entryPoint: string, + args: RuntimeArgs + ) { + return ExecutableDeployItem.fromExecutableDeployItemInternal( + new StoredContractByName(name, entryPoint, args) + ); + } + + public static newStoredVersionContractByHash( + hash: Uint8Array, + version: number | null, + entryPoint: string, + args: RuntimeArgs + ) { + return ExecutableDeployItem.fromExecutableDeployItemInternal( + new StoredVersionedContractByHash(hash, version, entryPoint, args) + ); + } + + public static newStoredVersionContractByName( + name: string, + version: number | null, + entryPoint: string, + args: RuntimeArgs + ) { + return ExecutableDeployItem.fromExecutableDeployItemInternal( + new StoredVersionedContractByName(name, version, entryPoint, args) + ); + } /** * Constructor for Transfer deploy item. @@ -337,14 +592,13 @@ export class Transfer extends ExecutableDeployItem { * transfer will be used as the source purse * @param id user-defined transfer id */ - constructor( + public static newTransfer( amount: BigNumberish, target: URef | PublicKey, sourcePurse?: URef, id: number | null = null ) { - super(); - const runtimeArgs = new RuntimeArgs([]); + const runtimeArgs = RuntimeArgs.fromMap({}); runtimeArgs.insert('amount', CLValue.u512(amount)); if (sourcePurse) { runtimeArgs.insert('source', CLValue.uref(sourcePurse)); @@ -364,17 +618,122 @@ export class Transfer extends ExecutableDeployItem { CLValue.option(CLTypedAndToBytesHelper.u64(id), CLTypeHelper.u64()) ); } - this.args = runtimeArgs.toBytes(); + return ExecutableDeployItem.fromExecutableDeployItemInternal( + new Transfer(runtimeArgs) + ); } - public toBytes(): ByteArray { - return concat([Uint8Array.from([this.tag]), toBytesArrayU8(this.args)]); + public isModuleBytes(): boolean { + return !!this.moduleBytes; + } + + public asModuleBytes(): ModuleBytes | undefined { + return this.moduleBytes; + } + + public isStoredContractByHash(): boolean { + return !!this.storedContractByHash; + } + + public asStoredContractByHash(): StoredContractByHash | undefined { + return this.storedContractByHash; + } + + public isStoredContractByName(): boolean { + return !!this.storedContractByName; + } + + public asStoredContractByName(): StoredContractByName | undefined { + return this.storedContractByName; + } + + public isStoredVersionContractByName(): boolean { + return !!this.storedVersionedContractByName; + } + + public asStoredVersionContractByName(): + | StoredVersionedContractByName + | undefined { + return this.storedVersionedContractByName; + } + + public isStoredVersionContractByHash(): boolean { + return !!this.storedVersionedContractByHash; + } + + public asStoredVersionContractByHash(): + | StoredVersionedContractByHash + | undefined { + return this.storedVersionedContractByHash; + } + + public isTransfer() { + return !!this.transfer; + } + + public asTransfer(): Transfer | undefined { + return this.transfer; } +} - public toJson(): Record { - return { - Transfer: { args: encodeBase16(this.args) } - }; +/** + * A deploy containing a smart contract along with the requester's signature(s). + */ +@jsonObject +export class Deploy { + @jsonMember({ + serializer: byteArrayJsonSerializer, + deserializer: byteArrayJsonDeserializer + }) + public hash: ByteArray; + + @jsonMember({ constructor: DeployHeader }) + public header: DeployHeader; + + @jsonMember({ + constructor: ExecutableDeployItem + }) + public payment: ExecutableDeployItem; + + @jsonMember({ + constructor: ExecutableDeployItem + }) + public session: ExecutableDeployItem; + + @jsonArrayMember(Approval) + public approvals: Approval[]; + + /** + * + * @param hash The DeployHash identifying this Deploy + * @param header The deployHeader + * @param payment The ExecutableDeployItem for payment code. + * @param session the ExecutableDeployItem for session code. + * @param approvals An array of signature and public key of the signers, who approve this deploy + */ + constructor( + hash: ByteArray, + header: DeployHeader, + payment: ExecutableDeployItem, + session: ExecutableDeployItem, + approvals: Approval[] + ) { + this.approvals = approvals; + this.session = session; + this.payment = payment; + this.header = header; + this.hash = hash; + } + + public isTransfer(): boolean { + return this.session.isTransfer(); + } + + public isStandardPayment(): boolean { + if (this.payment.isModuleBytes()) { + return this.payment.asModuleBytes()?.moduleBytes.length === 0; + } + return false; } } @@ -383,7 +742,7 @@ export class Transfer extends ExecutableDeployItem { * @param deployHeader */ export const serializeHeader = (deployHeader: DeployHeader) => { - return toBytesDeployHeader(deployHeader); + return deployHeader.toBytes(); }; /** @@ -446,24 +805,18 @@ export function makeDeploy( const serializedBody = serializeBody(payment, session); const bodyHash = blake.blake2b(serializedBody, null, 32); - const header: DeployHeader = { - account: deployParam.accountPublicKey, + const header: DeployHeader = new DeployHeader( + deployParam.accountPublicKey, + deployParam.timestamp!, + deployParam.ttl, + deployParam.gasPrice, bodyHash, - chainName: deployParam.chainName, - dependencies: deployParam.dependencies, - gasPrice: deployParam.gasPrice, - timestamp: deployParam.timestamp!, - ttl: deployParam.ttl - }; + deployParam.dependencies, + deployParam.chainName + ); const serializedHeader = serializeHeader(header); const deployHash = blake.blake2b(serializedHeader, null, 32); - return { - hash: deployHash, - header, - payment, - session, - approvals: [] - }; + return new Deploy(deployHash, header, payment, session, []); } /** @@ -523,12 +876,12 @@ export const setSignature = ( * * @param paymentAmount the number of motes paying to execution engine */ -export const standardPayment = (paymentAmount: bigint | JSBI) => { +export const standardPayment = (paymentAmount: BigNumberish) => { const paymentArgs = RuntimeArgs.fromMap({ amount: CLValue.u512(paymentAmount.toString()) }); - return new ModuleBytes(Uint8Array.from([]), paymentArgs.toBytes()); + return ExecutableDeployItem.newModuleBytes(Uint8Array.from([]), paymentArgs); }; /** @@ -537,27 +890,18 @@ export const standardPayment = (paymentAmount: bigint | JSBI) => { * @param deploy */ export const deployToJson = (deploy: Deploy) => { - const header = deploy.header; - const headerJson = { - account: header.account.toAccountHex(), - timestamp: new Date(header.timestamp).toISOString(), - ttl: humanizerTTL(deploy.header.ttl), - gas_price: header.gasPrice, - body_hash: encodeBase16(header.bodyHash), - dependencies: header.dependencies.map(it => encodeBase16(it)), - chain_name: header.chainName - }; - const json = { - hash: encodeBase16(deploy.hash), - header: headerJson, - payment: deploy.payment.toJson(), - session: deploy.session.toJson(), - approvals: deploy.approvals.map(it => { - return { - signer: it.signer, - signature: it.signature - }; - }) + const serializer = new TypedJSON(Deploy); + return { + deploy: serializer.stringify(deploy) }; - return { deploy: json }; +}; + +/** + * Convert the json to deploy object + * + * @param json + */ +export const deployFromJson = (json: any) => { + const serializer = new TypedJSON(Deploy); + return serializer.parse(json.deploy); }; diff --git a/packages/sdk/src/lib/Keys.ts b/packages/sdk/src/lib/Keys.ts index 487d0bae..0ab3c5a4 100644 --- a/packages/sdk/src/lib/Keys.ts +++ b/packages/sdk/src/lib/Keys.ts @@ -2,7 +2,8 @@ import * as fs from 'fs'; import * as nacl from 'tweetnacl-ts'; import { SignKeyPair, SignLength } from 'tweetnacl-ts'; import { decodeBase64 } from 'tweetnacl-util'; -import { encodeBase16, encodeBase64, PublicKey } from '../index'; +import { encodeBase16, encodeBase64 } from '../index'; +import { PublicKey } from '../lib/index'; import { byteHash } from './Contracts'; import { ec as EC } from 'elliptic'; import * as secp256k1 from 'ethereum-cryptography/secp256k1'; diff --git a/packages/sdk/src/lib/RuntimeArgs.ts b/packages/sdk/src/lib/RuntimeArgs.ts index acf6b470..54952ff5 100644 --- a/packages/sdk/src/lib/RuntimeArgs.ts +++ b/packages/sdk/src/lib/RuntimeArgs.ts @@ -2,8 +2,9 @@ * Implements a collection of runtime arguments. */ import { toBytesString, toBytesVecT } from './byterepr'; -import { CLValue, ToBytes } from './CLValue'; +import { CLValue, Result, StringValue, ToBytes, U32 } from './CLValue'; import { concat } from '@ethersproject/bytes'; +import { jsonMapMember, jsonObject } from 'typedjson'; export class NamedArg implements ToBytes { constructor(public name: string, public value: CLValue) {} @@ -11,23 +12,75 @@ export class NamedArg implements ToBytes { public toBytes(): ByteArray { return concat([toBytesString(this.name), this.value.toBytes()]); } + + public static fromBytes(bytes: Uint8Array): Result { + const nameRes = StringValue.fromBytes(bytes); + if (nameRes.hasError()) { + return Result.Err(nameRes.error); + } + const clValueRes = CLValue.fromBytes(nameRes.remainder); + if (clValueRes.hasError()) { + return Result.Err(clValueRes.error); + } + return Result.Ok( + new NamedArg(nameRes.value.val, clValueRes.value), + clValueRes.remainder + ); + } } +@jsonObject export class RuntimeArgs implements ToBytes { - constructor(private args: NamedArg[]) {} + @jsonMapMember(String, CLValue) + public args: Map; + + constructor(args: Map) { + this.args = args; + } public static fromMap(args: Record) { - const vec = Object.keys(args).map(a => { - return new NamedArg(a, args[a]); - }); - return new RuntimeArgs(vec); + const map: Map = new Map( + Object.keys(args).map(k => [k, args[k]]) + ); + return new RuntimeArgs(map); + } + + public static fromNamedArgs(namedArgs: NamedArg[]) { + const args = namedArgs.reduce>((pre, cur) => { + pre[cur.name] = cur.value; + return pre; + }, {}); + return RuntimeArgs.fromMap(args); } public insert(key: string, value: CLValue) { - this.args.push(new NamedArg(key, value)); + this.args.set(key, value); } public toBytes() { - return toBytesVecT(this.args); + const vec = Array.from(this.args.entries()).map((a: [string, CLValue]) => { + return new NamedArg(a[0], a[1]); + }); + + return toBytesVecT(vec); + } + + public static fromBytes(bytes: Uint8Array): Result { + const sizeRes = U32.fromBytes(bytes); + if (sizeRes.hasError()) { + return Result.Err(sizeRes.error); + } + const size = sizeRes.value.val.toNumber(); + let remainBytes = sizeRes.remainder; + const res: NamedArg[] = []; + for (let i = 0; i < size; i++) { + const namedArgRes = NamedArg.fromBytes(remainBytes); + if (namedArgRes.hasError()) { + return Result.Err(namedArgRes.error); + } + res.push(namedArgRes.value); + remainBytes = namedArgRes.remainder; + } + return Result.Ok(RuntimeArgs.fromNamedArgs(res), remainBytes); } } diff --git a/packages/sdk/src/lib/StoredValue.ts b/packages/sdk/src/lib/StoredValue.ts index 994e03d9..239b5adf 100644 --- a/packages/sdk/src/lib/StoredValue.ts +++ b/packages/sdk/src/lib/StoredValue.ts @@ -1,4 +1,5 @@ import { jsonArrayMember, jsonMember, jsonObject } from 'typedjson'; +import { CLValue } from './CLValue'; @jsonObject class NamedKey { @@ -29,7 +30,7 @@ class ActionThresholds { * Structure representing a user's account, stored in global state. */ @jsonObject -class SAccount { +class AccountJson { get accountHash(): string { return this._accountHash; } @@ -47,7 +48,7 @@ class SAccount { } @jsonObject -export class STransfer { +export class TransferJson { // Deploy that created the transfer @jsonMember({ name: 'deploy_hash', constructor: String }) public deployHash: string; @@ -78,7 +79,7 @@ export class STransfer { } @jsonObject -export class SDeployInfo { +export class DeployInfoJson { // The relevant Deploy. @jsonMember({ name: 'deploy_hash', constructor: String }) public deployHash: string; @@ -147,18 +148,19 @@ export class SeigniorageAllocation { * Auction metdata. Intended to be recorded at each era. */ @jsonObject -export class EraInfo { +export class EraInfoJson { @jsonArrayMember(SeigniorageAllocation, { name: 'seigniorage_allocations' }) public seigniorageAllocations: SeigniorageAllocation[]; } @jsonObject export class StoredValue { - // todo SCLValue; - + // StoredVale + @jsonMember({ constructor: CLValue }) + public CLValue?: CLValue; // An account - @jsonMember({ constructor: SAccount }) - public Account?: SAccount; + @jsonMember({ constructor: AccountJson }) + public Account?: AccountJson; // A contract's Wasm @jsonMember({ constructor: String }) @@ -173,13 +175,13 @@ export class StoredValue { public ContractPackage?: string; // A record of a transfer - @jsonMember({ constructor: STransfer }) - public Transfer?: STransfer; + @jsonMember({ constructor: TransferJson }) + public Transfer?: TransferJson; // A record of a deploy - @jsonMember({ constructor: SDeployInfo }) - public DeployInfo?: SDeployInfo; + @jsonMember({ constructor: DeployInfoJson }) + public DeployInfo?: DeployInfoJson; - @jsonMember({ constructor: EraInfo }) - public EraInfo?: EraInfo; + @jsonMember({ constructor: EraInfoJson }) + public EraInfo?: EraInfoJson; } diff --git a/packages/sdk/src/lib/option.ts b/packages/sdk/src/lib/option.ts index 5312cb5b..1fd7750a 100644 --- a/packages/sdk/src/lib/option.ts +++ b/packages/sdk/src/lib/option.ts @@ -3,6 +3,7 @@ import { CLType, CLTypedAndToBytes, CLTypeHelper, + CLValue, fromBytesByCLType, FromBytesError, OptionType, @@ -56,6 +57,18 @@ export class Option extends CLTypedAndToBytes { return this.t !== null; } + /** + * Extract value. + * + * @returns CLValue if the `Option` has some value. + */ + public getSome(): CLValue { + if (!this.isSome()) { + throw new Error('Value is None'); + } + return CLValue.fromT(this.t!); + } + /** * Serializes the `Option` into an array of bytes. */ @@ -75,7 +88,7 @@ export class Option extends CLTypedAndToBytes { if (u8Res.hasError()) { return Result.Err(u8Res.error); } - const optionTag = u8Res.value.value as number; + const optionTag = u8Res.value.val.toNumber(); if (optionTag === OPTION_TAG_NONE) { return Result.Ok(new Option(null, type.innerType), u8Res.remainder); } else if (optionTag === OPTION_TAG_SOME) { diff --git a/packages/sdk/src/services/CasperServiceByJsonRPC.ts b/packages/sdk/src/services/CasperServiceByJsonRPC.ts index af38178c..a91bc4b0 100644 --- a/packages/sdk/src/services/CasperServiceByJsonRPC.ts +++ b/packages/sdk/src/services/CasperServiceByJsonRPC.ts @@ -1,6 +1,8 @@ import Client, { HTTPTransport, RequestManager } from 'rpc-client-js'; import { DeployUtil, encodeBase16, PublicKey } from '..'; import { deployToJson } from '../lib/DeployUtil'; +import { TypedJSON } from 'typedjson'; +import { StoredValue } from '../lib/StoredValue'; interface RpcResult { api_version: string; @@ -214,8 +216,8 @@ export class CasperServiceByJsonRPC { stateRootHash, 'account-hash-' + accountHash, [] - ).then(res => res.stored_value.Account); - return account.main_purse; + ).then(res => res.Account!); + return account.mainPurse; } /** @@ -269,8 +271,8 @@ export class CasperServiceByJsonRPC { stateRootHash: string, key: string, path: string[] - ) { - return await this.client.request({ + ): Promise { + const res = await this.client.request({ method: 'state_get_item', params: { state_root_hash: stateRootHash, @@ -278,6 +280,14 @@ export class CasperServiceByJsonRPC { path } }); + if (res.error) { + return res; + } else { + const storedValueJson = res.stored_value; + const serializer = new TypedJSON(StoredValue); + const storedValue = serializer.parse(storedValueJson)!; + return storedValue; + } } public async deploy(signedDeploy: DeployUtil.Deploy) { diff --git a/packages/sdk/test/lib/CasperClient.test.ts b/packages/sdk/test/lib/CasperClient.test.ts index 59d9628a..c11d8f2c 100644 --- a/packages/sdk/test/lib/CasperClient.test.ts +++ b/packages/sdk/test/lib/CasperClient.test.ts @@ -3,9 +3,8 @@ import { CasperClient } from '../../src/lib/CasperClient'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { DeployUtil, Keys, PublicKey } from '../../src/lib'; -import { Ed25519, Secp256K1, SignatureAlgorithm } from '../../src/lib/Keys'; -import JSBI from 'jsbi'; +import { Keys } from '../../src/lib'; +import { Secp256K1, SignatureAlgorithm } from '../../src/lib/Keys'; import { decodeBase16 } from '../../src'; let casperClient: CasperClient; @@ -118,29 +117,7 @@ describe('CasperClient', () => { expect(loadedKeyPair.privateKey).to.deep.equal(edKeyPair.privateKey); }); - // todo move it to example once we publish transfer feature - describe.skip('transfer', async () => { - const transfer = new DeployUtil.Transfer( - 100000000000000, - PublicKey.fromHex( - '01a72eb5ba13e243d40e56b0547536e3ad1584eee5a386c7be5d5a1f94c09a6592' - ) - ); - const keyPair = Ed25519.parseKeyFiles( - '../server/test.public.key', - '../server/test.private.key' - ); - const deploy = casperClient.makeTransferDeploy( - new DeployUtil.DeployParams(keyPair.publicKey, 'casper-net-1'), - transfer, - DeployUtil.standardPayment(JSBI.BigInt(100000000000000)) - ); - const signedDeploy = casperClient.signDeploy(deploy, keyPair); - const deployHash = await casperClient.putDeploy(signedDeploy); - console.log(deployHash); - }); - - it('should create a HK wallet and derive child account correctly', function () { + it('should create a HK wallet and derive child account correctly', function() { const seed = 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542'; const hdKey = casperClient.newHdWallet(decodeBase16(seed)); diff --git a/packages/sdk/test/lib/DeployUtil.test.ts b/packages/sdk/test/lib/DeployUtil.test.ts new file mode 100644 index 00000000..f136ba45 --- /dev/null +++ b/packages/sdk/test/lib/DeployUtil.test.ts @@ -0,0 +1,82 @@ +import { expect, assert } from 'chai'; +import { Keys, DeployUtil } from '../../src/lib'; +import { TypedJSON } from 'typedjson'; + +describe('DeployUtil', () => { + it('should stringify/parse DeployHeader correctly', function() { + const ed25519Key = Keys.Ed25519.new(); + const deployHeader = new DeployUtil.DeployHeader( + ed25519Key.publicKey, + 123456, + 654321, + 10, + Uint8Array.from(Array(32).fill(42)), + [Uint8Array.from(Array(32).fill(2))], + 'test-network' + ); + const serializer = new TypedJSON(DeployUtil.DeployHeader); + const json = serializer.stringify(deployHeader); + const deployHeader1 = serializer.parse(json); + expect(deployHeader1).to.deep.equal(deployHeader); + }); + + it('should allow to extract data from Transfer', function() { + const senderKey = Keys.Ed25519.new(); + const recipientKey = Keys.Ed25519.new(); + const networkName = 'test-network'; + const paymentAmount = 10000000000000; + const transferAmount = 10; + const id = 34; + + let deployParams = new DeployUtil.DeployParams( + senderKey.publicKey, + networkName + ); + let session = DeployUtil.ExecutableDeployItem.newTransfer( + transferAmount, + recipientKey.publicKey, + undefined, + id + ); + let payment = DeployUtil.standardPayment(paymentAmount); + let deploy = DeployUtil.makeDeploy(deployParams, session, payment); + deploy = DeployUtil.signDeploy(deploy, senderKey); + deploy = DeployUtil.signDeploy(deploy, recipientKey); + + let json = DeployUtil.deployToJson(deploy); + deploy = DeployUtil.deployFromJson(json)!; + + assert.isTrue(deploy.isTransfer()); + assert.isTrue(deploy.isStandardPayment()); + assert.deepEqual(deploy.header.account, senderKey.publicKey); + assert.deepEqual( + deploy.payment + .getArgByName('amount')! + .asBigNumber() + .toNumber(), + paymentAmount + ); + assert.deepEqual( + deploy.session + .getArgByName('amount')! + .asBigNumber() + .toNumber(), + transferAmount + ); + assert.deepEqual( + deploy.session.getArgByName('target')!.asBytesArray(), + recipientKey.accountHash() + ); + assert.deepEqual( + deploy.session + .getArgByName('id')! + .asOption() + .getSome() + .asBigNumber() + .toNumber(), + id + ); + assert.deepEqual(deploy.approvals[0].signer, senderKey.accountHex()); + assert.deepEqual(deploy.approvals[1].signer, recipientKey.accountHex()); + }); +}); diff --git a/packages/sdk/test/lib/Keys.test.ts b/packages/sdk/test/lib/Keys.test.ts index 43ff91bd..59ba6266 100644 --- a/packages/sdk/test/lib/Keys.test.ts +++ b/packages/sdk/test/lib/Keys.test.ts @@ -166,7 +166,6 @@ describe('Secp256K1', () => { // expect we could sign the message and verify the signature later. const message = Buffer.from('hello world'); const signature = signKeyPair.sign(Buffer.from(message)); - console.log(encodeBase16(signature)); // expect we could verify the signature created by ourself expect(signKeyPair.verfiy(signature, message)).to.equal(true); }); diff --git a/packages/sdk/test/lib/RuntimeArgs.test.ts b/packages/sdk/test/lib/RuntimeArgs.test.ts index 8ffe79df..3ef16770 100644 --- a/packages/sdk/test/lib/RuntimeArgs.test.ts +++ b/packages/sdk/test/lib/RuntimeArgs.test.ts @@ -1,7 +1,7 @@ -import { expect } from 'chai'; -import { CLValue, RuntimeArgs } from '../../src/lib'; +import { expect, assert } from 'chai'; +import { CLValue, RuntimeArgs, CLTypedAndToBytesHelper } from '../../src/lib'; import { decodeBase16 } from '../../src'; -import { toBytesU32 } from '../../src/lib/byterepr'; +import { TypedJSON } from 'typedjson'; describe(`RuntimeArgs`, () => { it('should serialize RuntimeArgs correctly', () => { @@ -38,8 +38,48 @@ describe(`RuntimeArgs`, () => { it('should serialize empty NamedArgs correctly', () => { const truth = decodeBase16('00000000'); const runtimeArgs = RuntimeArgs.fromMap({}); - console.log(toBytesU32(0)); const bytes = runtimeArgs.toBytes(); expect(bytes).to.deep.eq(truth); }); + + it('should deserialize U512', () => { + let value = CLValue.u512(43000000000); + let serializer = new TypedJSON(CLValue); + let str = serializer.stringify(value); + assert.deepEqual(value.asBigNumber(), serializer.parse(str)!.asBigNumber()); + }); + + it('should deserialize Option of U512', () => { + let a = CLTypedAndToBytesHelper.u512(123); + let value = CLValue.option(a, a.clType()); + let serializer = new TypedJSON(CLValue); + let str = serializer.stringify(value); + let parsed = serializer.parse(str)!; + assert.deepEqual( + value + .asOption() + .getSome() + .asBigNumber(), + parsed + .asOption() + .getSome() + .asBigNumber() + ); + }); + + it('should deserialize RuntimeArgs', () => { + let a = CLTypedAndToBytesHelper.u512(123); + const runtimeArgs = RuntimeArgs.fromMap({ + a: CLValue.option(null, a.clType()) + }); + let serializer = new TypedJSON(RuntimeArgs); + let str = serializer.stringify(runtimeArgs); + let value = serializer.parse(str)!; + assert.isTrue( + value.args + .get('a')! + .asOption() + .isNone() + ); + }); }); diff --git a/packages/server/package.json b/packages/server/package.json index ca20d35d..4f369d0c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -40,7 +40,7 @@ "@types/http-proxy-middleware": "^0.19.3", "auth0": "^2.28.0", "blakejs": "^1.1.0", - "casper-client-sdk": "1.0.9", + "casper-client-sdk": "file:./../sdk", "command-line-args": "^5.1.1", "cron": "1.7.2", "dotenv": "^8.0.0", diff --git a/packages/server/src/StoredFaucetService.ts b/packages/server/src/StoredFaucetService.ts index 4b244892..7197ab2e 100644 --- a/packages/server/src/StoredFaucetService.ts +++ b/packages/server/src/StoredFaucetService.ts @@ -1,8 +1,4 @@ -import { - encodeBase16, - CasperServiceByJsonRPC, - DeployUtil -} from 'casper-client-sdk'; +import { CasperServiceByJsonRPC, DeployUtil, encodeBase16 } from 'casper-client-sdk'; import { ByteArray } from 'tweetnacl-ts'; import { CallFaucet } from './lib/Contracts'; import { AsymmetricKey } from 'casper-client-sdk/dist/lib/Keys'; @@ -18,7 +14,8 @@ export class StoredFaucetService { private transferAmount: bigint, private casperService: CasperServiceByJsonRPC, private chainName: string - ) {} + ) { + } async callStoredFaucet(accountPublicKeyHash: ByteArray): Promise { const state = await this.checkState(); @@ -26,14 +23,14 @@ export class StoredFaucetService { const sessionArgs = CallFaucet.args( accountPublicKeyHash, this.transferAmount - ).toBytes(); - const session = new DeployUtil.StoredContractByName( + ); + const session = DeployUtil.ExecutableDeployItem.newStoredContractByName( CONTRACT_NAME, ENTRY_POINT_NAME, sessionArgs ); - const payment = DeployUtil.standardPayment(this.paymentAmount); + const payment = DeployUtil.standardPayment(this.paymentAmount.toString()); const deployByName = DeployUtil.makeDeploy( new DeployUtil.DeployParams( this.contractKeys.publicKey, @@ -49,7 +46,7 @@ export class StoredFaucetService { await this.casperService.deploy(signedDeploy); return signedDeploy.hash; } else { - throw new Error("Can't do faucet now"); + throw new Error('Can\'t do faucet now'); } } diff --git a/packages/signer/package.json b/packages/signer/package.json index 5aebe405..7906113d 100644 --- a/packages/signer/package.json +++ b/packages/signer/package.json @@ -83,10 +83,7 @@ "trailingComma": "none" }, "lint-staged": { - "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ - "prettier --write", - "git add" - ] + "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": "prettier --write" }, "devDependencies": { "@types/file-saver": "^2.0.1", diff --git a/packages/ui/package.json b/packages/ui/package.json index 870598ab..40992591 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -21,7 +21,7 @@ "@types/react-router-dom": "^5.0.1", "bootstrap": "^4.3.1", "bootstrap-components": "^1.1.287", - "casper-client-sdk": "^1.0.9", + "casper-client-sdk": "file:./../sdk", "change-case": "^4.1.1", "chart.js": "^2.9.3", "d3": "^5.9.7", diff --git a/packages/ui/src/containers/DeployContractsContainer.ts b/packages/ui/src/containers/DeployContractsContainer.ts index af1b5800..52e9d406 100644 --- a/packages/ui/src/containers/DeployContractsContainer.ts +++ b/packages/ui/src/containers/DeployContractsContainer.ts @@ -362,35 +362,31 @@ export class DeployContractsContainer { const args = deployArguments.value; let session: ByteArray | string; - let runtimeArgs = new RuntimeArgs( + let runtimeArgs = RuntimeArgs.fromNamedArgs( args.map((arg: FormState) => { return DeployArgumentParser.buildArgument(arg); }) ); - const paymentAmount = JSBI.BigInt(config.paymentAmount.value); let sessionExecutionItem: DeployUtil.ExecutableDeployItem | null = null; if (config.contractType.value === DeployUtil.ContractType.WASM) { session = this.selectedFileContent!; - sessionExecutionItem = new DeployUtil.ModuleBytes( - session, - runtimeArgs.toBytes() - ); + sessionExecutionItem = DeployUtil.ExecutableDeployItem.newModuleBytes(session, runtimeArgs); } else if (config.contractType.value === DeployUtil.ContractType.Hash) { session = decodeBase16(config.contractHash.value); const entryPoint = config.entryPoint.value; - sessionExecutionItem = new DeployUtil.StoredContractByHash( + sessionExecutionItem = DeployUtil.ExecutableDeployItem.newStoredContractByHash( session, entryPoint, - runtimeArgs.toBytes() + runtimeArgs ); } else if (config.contractType.value === DeployUtil.ContractType.Name) { session = config.contractName.value; const entryPoint = config.entryPoint.value; - sessionExecutionItem = new DeployUtil.StoredContractByName( + sessionExecutionItem = DeployUtil.ExecutableDeployItem.newStoredContractByName( session, entryPoint, - runtimeArgs.toBytes() + runtimeArgs ); } @@ -401,7 +397,7 @@ export class DeployContractsContainer { window.config.network?.chainName || '' ), sessionExecutionItem, - DeployUtil.standardPayment(paymentAmount) + DeployUtil.standardPayment(config.paymentAmount.value) ); } return Promise.resolve(null); diff --git a/yarn.lock b/yarn.lock index f95d2d8f..3d5a1230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5560,6 +5560,25 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +"casper-client-sdk@file:./packages/sdk": + version "1.0.11" + dependencies: + "@ethersproject/bignumber" "^5.0.8" + "@ethersproject/bytes" "^5.0.5" + "@ethersproject/constants" "^5.0.5" + axios "^0.21.1" + blakejs "^1.1.0" + ethereum-cryptography "^0.1.3" + humanize-duration "^3.24.0" + jsbi "^3.1.2" + key-encoder "^2.0.3" + reflect-metadata "^0.1.13" + rpc-client-js "^1.0.2" + rxjs "^6.5.3" + tweetnacl-ts "^1.0.3" + tweetnacl-util "^0.15.0" + typedjson "^1.6.0-rc2" + casper-client-sdk@latest: version "1.0.8" resolved "https://registry.yarnpkg.com/casper-client-sdk/-/casper-client-sdk-1.0.8.tgz#00efdee87e040677a39178311f0f6ad127f984a8"