From 0a34a8c1c0ba695ccb64c3360266c030183741ba Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 13 Feb 2024 15:23:00 +0200 Subject: [PATCH 1/5] implement message class according to specs --- src/constants.ts | 2 + src/interface.ts | 49 +++++++++++--- src/message.spec.ts | 87 ++++++++++++++++++++++++ src/message.ts | 93 ++++++++++++++++++++++++++ src/signableMessage.ts | 147 +++++++++++++++++++++-------------------- src/transaction.ts | 2 +- 6 files changed, 295 insertions(+), 85 deletions(-) create mode 100644 src/message.spec.ts create mode 100644 src/message.ts diff --git a/src/constants.ts b/src/constants.ts index c00f61a3..63784a2f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,3 +14,5 @@ export const CONTRACT_DEPLOY_ADDRESS = "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq export const DELEGATION_MANAGER_SC_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6"; export const DEFAULT_HRP = "erd"; export const ESDT_CONTRACT_ADDRESS = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzllls8a5w6u"; +export const DEFAULT_MESSAGE_VERSION = 1; +export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n"; diff --git a/src/interface.ts b/src/interface.ts index 0fe96076..70bdae7f 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -26,16 +26,36 @@ export interface IPlainTransactionObject { guardianSignature?: string; } -export interface ISignature { hex(): string; } -export interface IAddress { bech32(): string; } -export interface ITransactionValue { toString(): string; } -export interface IAccountBalance { toString(): string; } -export interface INonce { valueOf(): number; } -export interface IChainID { valueOf(): string; } -export interface IGasLimit { valueOf(): number; } -export interface IGasPrice { valueOf(): number; } -export interface ITransactionVersion { valueOf(): number; } -export interface ITransactionOptions { valueOf(): number; } +export interface ISignature { + hex(): string; +} +export interface IAddress { + bech32(): string; +} +export interface ITransactionValue { + toString(): string; +} +export interface IAccountBalance { + toString(): string; +} +export interface INonce { + valueOf(): number; +} +export interface IChainID { + valueOf(): string; +} +export interface IGasLimit { + valueOf(): number; +} +export interface IGasPrice { + valueOf(): number; +} +export interface ITransactionVersion { + valueOf(): number; +} +export interface ITransactionOptions { + valueOf(): number; +} export interface ITransactionPayload { length(): number; @@ -72,4 +92,11 @@ export interface ITransactionNext { guardian: string; signature: Uint8Array; guardianSignature: Uint8Array; - } +} + +export interface IMessage { + data: Uint8Array; + signature?: Uint8Array; + address?: IAddress; + version?: number; +} diff --git a/src/message.spec.ts b/src/message.spec.ts new file mode 100644 index 00000000..3abe9cef --- /dev/null +++ b/src/message.spec.ts @@ -0,0 +1,87 @@ +import { assert } from "chai"; +import { Message, MessageComputer } from "./message"; +import { loadTestWallets, TestWallet } from "./testutils"; +import { UserVerifier } from "@multiversx/sdk-wallet"; +import { DEFAULT_MESSAGE_VERSION } from "./constants"; + +describe("test message", () => { + let alice: TestWallet; + const messageComputer = new MessageComputer(); + + before(async function () { + ({ alice } = await loadTestWallets()); + }); + + it("should test message compute bytes for signing", async () => { + const data = Buffer.from("test message"); + + const message = new Message({ + data: data, + }); + + const serialized = messageComputer.computeBytesForSigning(message); + + assert.equal( + Buffer.from(serialized).toString("hex"), + "2162d6271208429e6d3e664139e98ba7c5f1870906fb113e8903b1d3f531004d", + ); + }); + + it("should create, sign, pack, unpack and verify message", async () => { + const data = Buffer.from("test"); + + const message = new Message({ + data: data, + address: alice.getAddress(), + }); + + message.signature = await alice.signer.sign(Buffer.from(messageComputer.computeBytesForSigning(message))); + + assert.equal( + Buffer.from(message.signature).toString("hex"), + "7aff43cd6e3d880a65033bf0a1b16274854fd7dfa9fe5faa7fa9a665ee851afd4c449310f5f1697d348e42d1819eaef69080e33e7652d7393521ed50d7427a0e", + ); + + const packedMessage = messageComputer.packMessage(message); + assert.deepEqual(packedMessage, { + address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + message: "74657374", + signature: + "7aff43cd6e3d880a65033bf0a1b16274854fd7dfa9fe5faa7fa9a665ee851afd4c449310f5f1697d348e42d1819eaef69080e33e7652d7393521ed50d7427a0e", + version: 1, + }); + + const unpackedMessage = messageComputer.unpackMessage(packedMessage); + assert.deepEqual(unpackedMessage.address, alice.getAddress()); + assert.deepEqual(unpackedMessage.data, message.data); + assert.deepEqual(unpackedMessage.signature, message.signature); + assert.deepEqual(unpackedMessage.version, message.version); + + const verifier = UserVerifier.fromAddress(alice.getAddress()); + const isValid = verifier.verify( + Buffer.from(messageComputer.computeBytesForVerifying(unpackedMessage)), + Buffer.from(unpackedMessage.signature!), + ); + assert.equal(isValid, true); + }); + + it("should unpack legacy message", async () => { + const legacyMessage = { + address: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + message: "0x7468697320697320612074657374206d657373616765", + signature: + "0xb16847437049986f936dd4a0917c869730cbf29e40a0c0821ca70db33f44758c3d41bcbea446dee70dea13d50942343bb78e74979dc434bbb2b901e0f4fd1809", + version: 1, + signer: "ErdJS", + }; + + const message = messageComputer.unpackMessage(legacyMessage); + assert.deepEqual(message.address, alice.getAddress()); + assert.deepEqual(Buffer.from(message.data).toString(), "this is a test message"); + assert.deepEqual( + Buffer.from(message.signature!).toString("hex"), + "b16847437049986f936dd4a0917c869730cbf29e40a0c0821ca70db33f44758c3d41bcbea446dee70dea13d50942343bb78e74979dc434bbb2b901e0f4fd1809", + ); + assert.deepEqual(message.version, DEFAULT_MESSAGE_VERSION); + }); +}); diff --git a/src/message.ts b/src/message.ts new file mode 100644 index 00000000..bc0fe97f --- /dev/null +++ b/src/message.ts @@ -0,0 +1,93 @@ +import { IAddress, IMessage } from "./interface"; +import { DEFAULT_MESSAGE_VERSION, MESSAGE_PREFIX } from "./constants"; +import { Address } from "./address"; + +const createKeccakHash = require("keccak"); + +export class Message { + /** + * Actual message being signed. + */ + public data: Uint8Array; + /** + * The message signature. + */ + public signature?: Uint8Array; + /** + * Address of the wallet that performed the signing operation. + */ + public address?: IAddress; + /** + * Number representing the message version. + */ + public version?: number; + + constructor(options: { data: Uint8Array; signature?: Uint8Array; address?: IAddress; version?: number }) { + this.data = options.data; + this.signature = options.signature; + this.address = options.address; + this.version = options.version ? options.version : DEFAULT_MESSAGE_VERSION; + } +} + +export class MessageComputer { + constructor() {} + + computeBytesForSigning(message: IMessage): Uint8Array { + const messageSize = Buffer.from(message.data.length.toString()); + const signableMessage = Buffer.concat([messageSize, message.data]); + let bytesToHash = Buffer.concat([Buffer.from(MESSAGE_PREFIX), signableMessage]); + + return createKeccakHash("keccak256").update(bytesToHash).digest(); + } + + computeBytesForVerifying(message: IMessage): Uint8Array { + return this.computeBytesForSigning(message); + } + + packMessage(message: IMessage): { + message: string; + signature: string; + address: string; + version: number; + } { + return { + message: Buffer.from(message.data).toString("hex"), + signature: message.signature ? Buffer.from(message.signature).toString("hex") : "", + address: message.address ? message.address.bech32() : "", + version: message.version ? message.version : DEFAULT_MESSAGE_VERSION, + }; + } + + unpackMessage(packedMessage: { message: string; signature?: string; address?: string; version?: number }): Message { + let dataHex = this.trimHexPrefix(packedMessage.message); + const data = Buffer.from(dataHex, "hex"); + + let signature: Uint8Array | undefined = undefined; + if (packedMessage.signature) { + let signatureHex = this.trimHexPrefix(packedMessage.signature); + signature = Buffer.from(signatureHex, "hex"); + } + + let address: Address | undefined = undefined; + if (packedMessage.address) { + address = Address.fromBech32(packedMessage.address); + } + + const version = packedMessage.version || DEFAULT_MESSAGE_VERSION; + + return new Message({ + data: data, + signature: signature, + address: address, + version: version, + }); + } + + private trimHexPrefix(data: string): string { + if (data.startsWith("0x") || data.startsWith("0X")) { + return data.slice(2); + } + return data; + } +} diff --git a/src/signableMessage.ts b/src/signableMessage.ts index 4fe82b7e..9d22e54c 100644 --- a/src/signableMessage.ts +++ b/src/signableMessage.ts @@ -1,80 +1,81 @@ import { Address } from "./address"; import { ISignature } from "./interface"; import { interpretSignatureAsBuffer } from "./signature"; +import { MESSAGE_PREFIX } from "./constants"; const createKeccakHash = require("keccak"); -export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n"; - +/** + * @deprecated Use {@link Message} instead. + */ export class SignableMessage { - - /** - * Actual message being signed. - */ - message: Buffer; - /** - * Signature obtained by a signer of type @param signer . - */ - signature: Buffer; - - /** - * Address of the wallet that performed the signing operation - */ - address: Address; - - /** - * Text representing the identifer for the application that signed the message - */ - signer: string; - - /** - * Number representing the signable message version - */ - version: number; - - public constructor(init?: Partial) { - this.message = Buffer.from([]); - this.signature = Buffer.from([]); - this.version = 1; - this.signer = "ErdJS"; - this.address = new Address(); - - Object.assign(this, init); - } - - serializeForSigning(): Buffer { - const messageSize = Buffer.from(this.message.length.toString()); - const signableMessage = Buffer.concat([messageSize, this.message]); - let bytesToHash = Buffer.concat([Buffer.from(MESSAGE_PREFIX), signableMessage]); - - return createKeccakHash("keccak256").update(bytesToHash).digest(); - } - - serializeForSigningRaw(): Buffer { - return Buffer.concat([this.getMessageSize(), this.message]); - } - - getSignature(): Buffer { - return this.signature; - } - - applySignature(signature: ISignature | Uint8Array) { - this.signature = interpretSignatureAsBuffer(signature); - } - - getMessageSize(): Buffer { - const messageSize = Buffer.alloc(4); - messageSize.writeUInt32BE(this.message.length, 0); - - return messageSize; - } - - toJSON(): object { - return { - address: this.address.bech32(), - message: "0x" + this.message.toString("hex"), - signature: "0x" + this.signature.toString("hex"), - version: this.version, - signer: this.signer, - }; - } + /** + * Actual message being signed. + */ + message: Buffer; + /** + * Signature obtained by a signer of type @param signer . + */ + signature: Buffer; + + /** + * Address of the wallet that performed the signing operation + */ + address: Address; + + /** + * Text representing the identifer for the application that signed the message + */ + signer: string; + + /** + * Number representing the signable message version + */ + version: number; + + public constructor(init?: Partial) { + this.message = Buffer.from([]); + this.signature = Buffer.from([]); + this.version = 1; + this.signer = "ErdJS"; + this.address = new Address(); + + Object.assign(this, init); + } + + serializeForSigning(): Buffer { + const messageSize = Buffer.from(this.message.length.toString()); + const signableMessage = Buffer.concat([messageSize, this.message]); + let bytesToHash = Buffer.concat([Buffer.from(MESSAGE_PREFIX), signableMessage]); + + return createKeccakHash("keccak256").update(bytesToHash).digest(); + } + + serializeForSigningRaw(): Buffer { + return Buffer.concat([this.getMessageSize(), this.message]); + } + + getSignature(): Buffer { + return this.signature; + } + + applySignature(signature: ISignature | Uint8Array) { + this.signature = interpretSignatureAsBuffer(signature); + } + + getMessageSize(): Buffer { + const messageSize = Buffer.alloc(4); + messageSize.writeUInt32BE(this.message.length, 0); + + return messageSize; + } + + toJSON(): object { + return { + address: this.address.bech32(), + message: "0x" + this.message.toString("hex"), + signature: "0x" + this.signature.toString("hex"), + version: this.version, + signer: this.signer, + }; + } } diff --git a/src/transaction.ts b/src/transaction.ts index 9518e31a..971d1c7a 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -653,7 +653,7 @@ export class TransactionComputer { const serialized = JSON.stringify(plainTransaction); - return new Uint8Array(Buffer.from(serialized)); + return Buffer.from(serialized); } computeTransactionHash(transaction: ITransactionNext): Uint8Array { From 65230d03e6d03c3b3f029df55c22ee02ee68d9ae Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 13 Feb 2024 15:26:05 +0200 Subject: [PATCH 2/5] add empty lines --- src/interface.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/interface.ts b/src/interface.ts index 70bdae7f..65a0099e 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -29,30 +29,39 @@ export interface IPlainTransactionObject { export interface ISignature { hex(): string; } + export interface IAddress { bech32(): string; } + export interface ITransactionValue { toString(): string; } + export interface IAccountBalance { toString(): string; } + export interface INonce { valueOf(): number; } + export interface IChainID { valueOf(): string; } + export interface IGasLimit { valueOf(): number; } + export interface IGasPrice { valueOf(): number; } + export interface ITransactionVersion { valueOf(): number; } + export interface ITransactionOptions { valueOf(): number; } From e7e10feb50c85bca92f95c00409dec873bc24f08 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 13 Feb 2024 17:19:43 +0200 Subject: [PATCH 3/5] add short description for the transaction factories --- src/transactionsFactories/delegationTransactionsFactory.ts | 3 +++ src/transactionsFactories/relayedTransactionsFactory.ts | 3 +++ src/transactionsFactories/smartContractTransactionsFactory.ts | 3 +++ .../tokenManagementTransactionsFactory.ts | 3 +++ src/transactionsFactories/transferTransactionsFactory.ts | 3 +++ 5 files changed, 15 insertions(+) diff --git a/src/transactionsFactories/delegationTransactionsFactory.ts b/src/transactionsFactories/delegationTransactionsFactory.ts index 9e389ab6..b0b354d8 100644 --- a/src/transactionsFactories/delegationTransactionsFactory.ts +++ b/src/transactionsFactories/delegationTransactionsFactory.ts @@ -24,6 +24,9 @@ interface IValidatorPublicKey { hex(): string; } +/** + * Use this class to create delegation related transactions like creating a new delegation contract or adding nodes. + */ export class DelegationTransactionsFactory { private readonly config: Config; diff --git a/src/transactionsFactories/relayedTransactionsFactory.ts b/src/transactionsFactories/relayedTransactionsFactory.ts index 8893ccd3..5949e0dd 100644 --- a/src/transactionsFactories/relayedTransactionsFactory.ts +++ b/src/transactionsFactories/relayedTransactionsFactory.ts @@ -13,6 +13,9 @@ interface IConfig { gasLimitPerByte: BigNumber.Value; } +/** + * Use this class to create both RelyedV1 and RelayedV2 transactions. + */ export class RelayedTransactionsFactory { private readonly config: IConfig; diff --git a/src/transactionsFactories/smartContractTransactionsFactory.ts b/src/transactionsFactories/smartContractTransactionsFactory.ts index e64e2e77..2c328e08 100644 --- a/src/transactionsFactories/smartContractTransactionsFactory.ts +++ b/src/transactionsFactories/smartContractTransactionsFactory.ts @@ -27,6 +27,9 @@ interface TokenComputer { isFungible(token: Token): boolean; } +/** + * Use this class to create transactions to deploy, call or upgrade a smart contract. + */ export class SmartContractTransactionsFactory { private readonly config: Config; private readonly abiRegistry?: Abi; diff --git a/src/transactionsFactories/tokenManagementTransactionsFactory.ts b/src/transactionsFactories/tokenManagementTransactionsFactory.ts index 508c1e28..272fb599 100644 --- a/src/transactionsFactories/tokenManagementTransactionsFactory.ts +++ b/src/transactionsFactories/tokenManagementTransactionsFactory.ts @@ -29,6 +29,9 @@ interface Config { type RegisterAndSetAllRolesTokenType = "NFT" | "SFT" | "META" | "FNG"; +/** + * Use this class to create token management transactions like issuing ESDTs, creating NFTs, setting roles, etc. + */ export class TokenManagementTransactionsFactory { private readonly config: Config; private readonly trueAsHex: string; diff --git a/src/transactionsFactories/transferTransactionsFactory.ts b/src/transactionsFactories/transferTransactionsFactory.ts index ad606c65..29ea1573 100644 --- a/src/transactionsFactories/transferTransactionsFactory.ts +++ b/src/transactionsFactories/transferTransactionsFactory.ts @@ -22,6 +22,9 @@ interface TokenComputer { isFungible(token: Token): boolean; } +/** + * Use this class to create transactions for native token transfers (EGLD) or custom tokens transfers (ESDT/NTF/MetaESDT). + */ export class NextTransferTransactionsFactory { private readonly config: IConfig; private readonly dataArgsBuilder: TokenTransfersDataBuilder; From 9b7f2f25105e5fabb6b2c117449cd9bb27038161 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Wed, 14 Feb 2024 10:45:05 +0200 Subject: [PATCH 4/5] fixes after review --- src/message.ts | 11 ++++------- .../relayedTransactionsFactory.ts | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/message.ts b/src/message.ts index bc0fe97f..87725132 100644 --- a/src/message.ts +++ b/src/message.ts @@ -20,7 +20,7 @@ export class Message { /** * Number representing the message version. */ - public version?: number; + public version: number; constructor(options: { data: Uint8Array; signature?: Uint8Array; address?: IAddress; version?: number }) { this.data = options.data; @@ -60,14 +60,11 @@ export class MessageComputer { } unpackMessage(packedMessage: { message: string; signature?: string; address?: string; version?: number }): Message { - let dataHex = this.trimHexPrefix(packedMessage.message); + const dataHex = this.trimHexPrefix(packedMessage.message); const data = Buffer.from(dataHex, "hex"); - let signature: Uint8Array | undefined = undefined; - if (packedMessage.signature) { - let signatureHex = this.trimHexPrefix(packedMessage.signature); - signature = Buffer.from(signatureHex, "hex"); - } + const signatureHex = this.trimHexPrefix(packedMessage.signature || ""); + const signature = Buffer.from(signatureHex, "hex"); let address: Address | undefined = undefined; if (packedMessage.address) { diff --git a/src/transactionsFactories/relayedTransactionsFactory.ts b/src/transactionsFactories/relayedTransactionsFactory.ts index 5949e0dd..a7ff15f8 100644 --- a/src/transactionsFactories/relayedTransactionsFactory.ts +++ b/src/transactionsFactories/relayedTransactionsFactory.ts @@ -14,7 +14,7 @@ interface IConfig { } /** - * Use this class to create both RelyedV1 and RelayedV2 transactions. + * Use this class to create both RelayedV1 and RelayedV2 transactions. */ export class RelayedTransactionsFactory { private readonly config: IConfig; From 80a4fcefd890b05788a7302b790267f93540de02 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Fri, 16 Feb 2024 14:13:03 +0200 Subject: [PATCH 5/5] fixes after review --- src/interface.ts | 7 ------- src/message.ts | 10 +++++----- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/interface.ts b/src/interface.ts index 65a0099e..663cfcec 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -102,10 +102,3 @@ export interface ITransactionNext { signature: Uint8Array; guardianSignature: Uint8Array; } - -export interface IMessage { - data: Uint8Array; - signature?: Uint8Array; - address?: IAddress; - version?: number; -} diff --git a/src/message.ts b/src/message.ts index 87725132..d018f26d 100644 --- a/src/message.ts +++ b/src/message.ts @@ -1,4 +1,4 @@ -import { IAddress, IMessage } from "./interface"; +import { IAddress } from "./interface"; import { DEFAULT_MESSAGE_VERSION, MESSAGE_PREFIX } from "./constants"; import { Address } from "./address"; @@ -26,14 +26,14 @@ export class Message { this.data = options.data; this.signature = options.signature; this.address = options.address; - this.version = options.version ? options.version : DEFAULT_MESSAGE_VERSION; + this.version = options.version || DEFAULT_MESSAGE_VERSION; } } export class MessageComputer { constructor() {} - computeBytesForSigning(message: IMessage): Uint8Array { + computeBytesForSigning(message: Message): Uint8Array { const messageSize = Buffer.from(message.data.length.toString()); const signableMessage = Buffer.concat([messageSize, message.data]); let bytesToHash = Buffer.concat([Buffer.from(MESSAGE_PREFIX), signableMessage]); @@ -41,11 +41,11 @@ export class MessageComputer { return createKeccakHash("keccak256").update(bytesToHash).digest(); } - computeBytesForVerifying(message: IMessage): Uint8Array { + computeBytesForVerifying(message: Message): Uint8Array { return this.computeBytesForSigning(message); } - packMessage(message: IMessage): { + packMessage(message: Message): { message: string; signature: string; address: string;