-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add Bitcoin standard memo encoding example for bitSmiley and unit tests #196
Changes from all commits
06c81bb
94759ee
cbffabf
951044d
2268242
0d8fa3e
74895bc
cbec33f
9b81a5a
0e7e637
be67ae8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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)"], | ||
}; |
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(); | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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", | ||
|
@@ -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", | ||
|
@@ -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", | ||
|
@@ -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" | ||
} | ||
} |
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"; | ||||||
ws4charlie marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
// 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)); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Replace To avoid importing Apply this diff: - const encodedReceiver = Buffer.from(Web3.utils.hexToBytes(fields.receiver));
+ const encodedReceiver = ethers.utils.arrayify(fields.receiver); 📝 Committable suggestion
Suggested change
|
||||||
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 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Implement proper test runner and reporting
Replace manual test execution with a proper test framework to get better error reporting, test filtering, and CI integration capabilities.