Skip to content

Commit

Permalink
feat: add Bitcoin standard memo encoding example for bitSmiley and un…
Browse files Browse the repository at this point in the history
…it tests (#196)

Co-authored-by: Denis Fadeev <[email protected]>
  • Loading branch information
ws4charlie and fadeev authored Nov 29, 2024
1 parent 856177b commit dbc21df
Show file tree
Hide file tree
Showing 8 changed files with 1,866 additions and 24 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Test

on:
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
registry-url: "https://registry.npmjs.org"

- name: Install Dependencies
run: yarn install

- name: Test
run: yarn test
5 changes: 5 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/?(*.)+(spec|test).ts?(x)"],
};
120 changes: 120 additions & 0 deletions memo.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
var assert = require("assert");
const {
Header,
FieldsV0,
EncodingFormat,
OpCode,
EncodeToBytes,
} = require("./memo");
const { Web3 } = require("web3");
const web3 = new Web3();

// Test data
const receiver = "0xEA9808f0Ac504d1F521B5BbdfC33e6f1953757a7";
const payload = new TextEncoder().encode("a payload");
const revertAddress = "tb1q6rufg6myrxurdn0h57d2qhtm9zfmjw2mzcm05q";

// Test case for memo ABI encoding
const testMemoAbi = () => {
// Create memo header
const header = new Header(
EncodingFormat.EncodingFmtABI,
OpCode.DepositAndCall
);

// Create memo fields
const fields = new FieldsV0(receiver, payload, revertAddress);

// Encode standard memo
const encodedMemo = EncodeToBytes(header, fields);
const encodedMemoHex = web3.utils.bytesToHex(encodedMemo).slice(2);

// Expected output
const expectedHex =
"5a001007" + // header
"000000000000000000000000ea9808f0ac504d1f521b5bbdfc33e6f1953757a7" + // receiver
"0000000000000000000000000000000000000000000000000000000000000060" + // payload offset
"00000000000000000000000000000000000000000000000000000000000000a0" + // revertAddress offset
"0000000000000000000000000000000000000000000000000000000000000009" + // payload length
"61207061796c6f61640000000000000000000000000000000000000000000000" + // payload
"000000000000000000000000000000000000000000000000000000000000002a" + // revertAddress length
"746231713672756667366d7972787572646e3068353764327168746d397a666d6a77326d7a636d30357100000000000000000000000000000000000000000000"; // revertAddress

// Compare with expected output
assert.strictEqual(
encodedMemoHex,
expectedHex,
"ABI encoding failed: encoded bytes do not match expected"
);

console.log("Test passed: testMemoAbi");
};

// Test case for memo compact short encoding
const testMemoCompactShort = () => {
// Create memo header
const header = new Header(
EncodingFormat.EncodingFmtCompactShort,
OpCode.DepositAndCall
);

// Create memo fields
const fields = new FieldsV0(receiver, payload, revertAddress);

// Encode standard memo
const encodedMemo = EncodeToBytes(header, fields);
const encodedMemoHex = web3.utils.bytesToHex(encodedMemo).slice(2);

// Expected output
const expectedHex =
"5a011007" + // header
"ea9808f0ac504d1f521b5bbdfc33e6f1953757a7" + // receiver
"0961207061796c6f6164" + // payload
"2a746231713672756667366d7972787572646e3068353764327168746d397a666d6a77326d7a636d303571"; // revertAddress

// Compare with expected output
assert.strictEqual(
encodedMemoHex,
expectedHex,
"Compact short encoding failed: encoded bytes do not match expected"
);

console.log("Test passed: testMemoCompactShort");
};

// Test case for memo compact long encoding
const testMemoCompactLong = () => {
// Create memo header
const header = new Header(
EncodingFormat.EncodingFmtCompactLong,
OpCode.DepositAndCall
);

// Create memo fields
const fields = new FieldsV0(receiver, payload, revertAddress);

// Encode standard memo
const encodedMemo = EncodeToBytes(header, fields);
const encodedMemoHex = web3.utils.bytesToHex(encodedMemo).slice(2);

// Expected output
const expectedHex =
"5a021007" + // header
"ea9808f0ac504d1f521b5bbdfc33e6f1953757a7" + // receiver
"090061207061796c6f6164" + // payload
"2a00746231713672756667366d7972787572646e3068353764327168746d397a666d6a77326d7a636d303571"; // revertAddress

// Compare with expected output
assert.strictEqual(
encodedMemoHex,
expectedHex,
"Compact long encoding failed: encoded bytes do not match expected"
);

console.log("Test passed: testMemoCompactLong");
};

// Run the test cases
testMemoAbi();
testMemoCompactShort();
testMemoCompactLong();
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"lint": "npm run lint:js && npm run lint:sol",
"docs": "rimraf docs && typedoc ./packages/client/src/index.ts",
"copy-templates": "npx cpx './packages/tasks/templates/**/*' ./dist/packages/tasks/templates",
"copy-types": "npx cpx './typechain-types/**/*' ./dist/typechain-types"
"copy-types": "npx cpx './typechain-types/**/*' ./dist/typechain-types",
"test": "yarn run jest"
},
"keywords": [],
"author": "ZetaChain",
Expand All @@ -46,6 +47,7 @@
"@typechain/hardhat": "^6.1.2",
"@types/chai": "^4.2.0",
"@types/isomorphic-fetch": "^0.0.38",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.14.202",
"@types/mocha": ">=9.1.0",
"@types/node": ">=12.0.0",
Expand All @@ -67,12 +69,14 @@
"eslint-plugin-typescript-sort-keys": "^2.3.0",
"hardhat-gas-reporter": "^1.0.8",
"http-server": "^14.1.1",
"jest": "^29.7.0",
"prettier": "^2.8.8",
"prettier-plugin-solidity": "^1.1.3",
"rimraf": "^5.0.1",
"sinon": "^15.1.0",
"solhint": "^3.4.1",
"solidity-coverage": "^0.8.0",
"ts-jest": "^29.2.5",
"ts-node": ">=8.0.0",
"typechain": "^8.1.0",
"typedoc": "^0.26.5",
Expand Down Expand Up @@ -106,7 +110,7 @@
"dotenv": "16.0.3",
"ecpair": "^2.1.0",
"envfile": "^6.18.0",
"ethers": "5.4.7",
"ethers": "^5.4.7",
"eventemitter3": "^5.0.1",
"form-data": "^4.0.0",
"handlebars": "4.7.7",
Expand All @@ -117,7 +121,8 @@
"ora": "5.4.1",
"spinnies": "^0.5.1",
"tiny-secp256k1": "^2.2.3",
"web3": "^4.15.0",
"ws": "^8.17.1"
},
"packageManager": "[email protected]+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}
}
140 changes: 140 additions & 0 deletions packages/client/src/encodeToBytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ethers } from "ethers";
import { Web3 } from "web3";
import { isAddress } from "web3-validator";

// Memo identifier byte
const MemoIdentifier = 0x5a;

// Enums
enum OpCode {
Deposit = 0b0000,
DepositAndCall = 0b0001,
Call = 0b0010,
Invalid = 0b0011,
}

enum EncodingFormat {
EncodingFmtABI = 0b0000,
EncodingFmtCompactShort = 0b0001,
EncodingFmtCompactLong = 0b0010,
}

// Header Class
class Header {
encodingFmt: EncodingFormat;
opCode: OpCode;

constructor(encodingFmt: EncodingFormat, opCode: OpCode) {
this.encodingFmt = encodingFmt;
this.opCode = opCode;
}
}

// FieldsV0 Class
class FieldsV0 {
receiver: string;
payload: Uint8Array;
revertAddress: string;

constructor(receiver: string, payload: Uint8Array, revertAddress: string) {
if (!isAddress(receiver)) {
throw new Error("Invalid receiver address");
}
this.receiver = receiver;
this.payload = payload;
this.revertAddress = revertAddress;
}
}

// Main Encoding Function
const encodeToBytes = (header: Header, fields: FieldsV0): Uint8Array => {
if (!header || !fields) {
throw new Error("Header and fields are required");
}

// Construct Header Bytes
const headerBytes = new Uint8Array(4);
headerBytes[0] = MemoIdentifier;
headerBytes[1] = (0x00 << 4) | (header.encodingFmt & 0x0f);
headerBytes[2] = ((header.opCode & 0x0f) << 4) | 0x00;
headerBytes[3] = 0b00000111;

// Encode Fields
let encodedFields: Uint8Array;
switch (header.encodingFmt) {
case EncodingFormat.EncodingFmtABI:
encodedFields = encodeFieldsABI(fields);
break;
case EncodingFormat.EncodingFmtCompactShort:
case EncodingFormat.EncodingFmtCompactLong:
encodedFields = encodeFieldsCompact(header.encodingFmt, fields);
break;
default:
throw new Error("Unsupported encoding format");
}

// Combine Header and Fields
return new Uint8Array(
Buffer.concat([Buffer.from(headerBytes), Buffer.from(encodedFields)])
);
};

// Helper: ABI Encoding
const encodeFieldsABI = (fields: FieldsV0): Uint8Array => {
const types = ["address", "bytes", "string"];
const values = [fields.receiver, fields.payload, fields.revertAddress];
const encodedData = ethers.utils.defaultAbiCoder.encode(types, values);
return Uint8Array.from(Buffer.from(encodedData.slice(2), "hex"));
};

// Helper: Compact Encoding
const encodeFieldsCompact = (
compactFmt: EncodingFormat,
fields: FieldsV0
): Uint8Array => {
const encodedReceiver = Buffer.from(Web3.utils.hexToBytes(fields.receiver));
const encodedPayload = encodeDataCompact(compactFmt, fields.payload);
const encodedRevertAddress = encodeDataCompact(
compactFmt,
new TextEncoder().encode(fields.revertAddress)
);

return new Uint8Array(
Buffer.concat([encodedReceiver, encodedPayload, encodedRevertAddress])
);
};

// Helper: Compact Data Encoding
const encodeDataCompact = (
compactFmt: EncodingFormat,
data: Uint8Array
): Uint8Array => {
const dataLen = data.length;
let encodedLength: Buffer;

switch (compactFmt) {
case EncodingFormat.EncodingFmtCompactShort:
if (dataLen > 255) {
throw new Error(
"Data length exceeds 255 bytes for EncodingFmtCompactShort"
);
}
encodedLength = Buffer.from([dataLen]);
break;
case EncodingFormat.EncodingFmtCompactLong:
if (dataLen > 65535) {
throw new Error(
"Data length exceeds 65535 bytes for EncodingFmtCompactLong"
);
}
encodedLength = Buffer.alloc(2);
encodedLength.writeUInt16LE(dataLen);
break;
default:
throw new Error("Unsupported compact format");
}

return Buffer.concat([encodedLength, data]);
};

export { encodeToBytes, EncodingFormat, FieldsV0, Header, OpCode };
1 change: 1 addition & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./client";
export * from "./encodeToBytes";
export * from "./evmCall";
export * from "./evmDeposit";
export * from "./evmDepositAndCall";
Expand Down
Loading

0 comments on commit dbc21df

Please sign in to comment.