@alephium/ledger-app 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/merkle-tree/proofs.json +1 -0
- package/dist/merkle-tree/token.json +1107 -0
- package/dist/src/ledger-app.d.ts +2 -3
- package/dist/src/ledger-app.js +35 -22
- package/dist/src/merkle.d.ts +9 -0
- package/dist/src/merkle.js +59 -0
- package/dist/src/serde.d.ts +3 -1
- package/dist/src/serde.js +19 -17
- package/dist/src/tx-encoder.d.ts +11 -0
- package/dist/src/tx-encoder.js +82 -0
- package/dist/src/types.d.ts +1 -0
- package/dist/src/types.js +3 -1
- package/dist/test/merkle.test.d.ts +1 -0
- package/dist/test/merkle.test.js +18 -0
- package/dist/test/tx-encoder.test.d.ts +1 -0
- package/dist/test/tx-encoder.test.js +73 -0
- package/dist/test/utils.d.ts +6 -3
- package/dist/test/utils.js +13 -4
- package/dist/test/wallet.test.js +113 -72
- package/merkle-tree/proofs.json +1 -0
- package/merkle-tree/token.json +1107 -0
- package/package.json +8 -6
- package/src/index.ts +1 -1
- package/src/ledger-app.ts +39 -31
- package/src/merkle.ts +63 -0
- package/src/serde.ts +19 -18
- package/src/tx-encoder.ts +90 -0
- package/src/types.ts +3 -1
- package/test/merkle.test.ts +20 -0
- package/test/tx-encoder.test.ts +80 -0
- package/test/utils.ts +9 -0
- package/test/wallet.test.ts +102 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alephium/ledger-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "GPL",
|
|
5
5
|
"types": "dist/src/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"clean:windows": "node -e \"if (process.platform === 'win32') process.exit(1)\" || , if exist dist rmdir /Q /S dist",
|
|
13
13
|
"lint": "eslint . --ext ts",
|
|
14
14
|
"lint:fix": "eslint . --fix --ext ts",
|
|
15
|
+
"test": "BACKEND=speculos jest -i --config ./jest-config.json",
|
|
15
16
|
"speculos-test": "BACKEND=speculos jest -i --config ./jest-config.json",
|
|
16
17
|
"device-test": "BACKEND=device jest -i --config ./jest-config.json",
|
|
17
18
|
"pub": "npm run build && npm publish --access public"
|
|
@@ -26,13 +27,14 @@
|
|
|
26
27
|
"trailingComma": "none"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@alephium/web3": "^1.
|
|
30
|
-
"@ledgerhq/hw-transport": "6.31.0"
|
|
30
|
+
"@alephium/web3": "^1.5.0",
|
|
31
|
+
"@ledgerhq/hw-transport": "6.31.0",
|
|
32
|
+
"blakejs": "^1.2.1"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
|
-
"@alephium/
|
|
34
|
-
"@alephium/web3-
|
|
35
|
-
"@alephium/
|
|
35
|
+
"@alephium/cli": "^1.5.0",
|
|
36
|
+
"@alephium/web3-test": "^1.5.0",
|
|
37
|
+
"@alephium/web3-wallet": "^1.5.0",
|
|
36
38
|
"@ledgerhq/hw-transport-node-hid": "6.29.1",
|
|
37
39
|
"@ledgerhq/hw-transport-node-speculos": "6.29.0",
|
|
38
40
|
"@types/elliptic": "^6.4.13",
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './types'
|
|
2
|
-
export * from './ledger-app'
|
|
2
|
+
export * from './ledger-app'
|
package/src/ledger-app.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Account, KeyType, addressFromPublicKey, encodeHexSignature, groupOfAddress } from '@alephium/web3'
|
|
1
|
+
import { Account, KeyType, addressFromPublicKey, binToHex, codec, encodeHexSignature, groupOfAddress } from '@alephium/web3'
|
|
2
2
|
import Transport, { StatusCodes } from '@ledgerhq/hw-transport'
|
|
3
3
|
import * as serde from './serde'
|
|
4
4
|
import { ec as EC } from 'elliptic'
|
|
5
|
-
import { TokenMetadata } from './types'
|
|
5
|
+
import { MAX_TOKEN_SIZE, MAX_TOKEN_SYMBOL_LENGTH, TokenMetadata } from './types'
|
|
6
|
+
import { encodeTokenMetadata, encodeUnsignedTx } from './tx-encoder'
|
|
7
|
+
import { merkleTokens } from './merkle'
|
|
6
8
|
|
|
7
9
|
const ec = new EC('secp256k1')
|
|
8
10
|
|
|
@@ -17,10 +19,7 @@ export enum INS {
|
|
|
17
19
|
export const GROUP_NUM = 4
|
|
18
20
|
export const HASH_LEN = 32
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
const MAX_PAYLOAD_SIZE = 255
|
|
22
|
-
|
|
23
|
-
export default class AlephiumApp {
|
|
22
|
+
export class AlephiumApp {
|
|
24
23
|
readonly transport: Transport
|
|
25
24
|
|
|
26
25
|
constructor(transport: Transport) {
|
|
@@ -71,34 +70,43 @@ export default class AlephiumApp {
|
|
|
71
70
|
return decodeSignature(response)
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
async signUnsignedTx(
|
|
75
|
-
path: string,
|
|
76
|
-
unsignedTx: Buffer,
|
|
77
|
-
tokenMetadata: TokenMetadata[] = []
|
|
78
|
-
): Promise<string> {
|
|
73
|
+
async signUnsignedTx(path: string, unsignedTx: Buffer): Promise<string> {
|
|
79
74
|
console.log(`unsigned tx size: ${unsignedTx.length}`)
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
75
|
+
const tokenMetadata = getTokenMetadata(unsignedTx)
|
|
76
|
+
serde.checkTokenMetadata(tokenMetadata)
|
|
77
|
+
const tokenMetadataFrames = encodeTokenMetadata(tokenMetadata)
|
|
78
|
+
const txFrames = encodeUnsignedTx(path, unsignedTx)
|
|
79
|
+
const allFrames = [...tokenMetadataFrames, ...txFrames]
|
|
80
|
+
|
|
81
|
+
let response: Buffer | undefined = undefined
|
|
82
|
+
for (const frame of allFrames) {
|
|
83
|
+
response = await this.transport.send(CLA, INS.SIGN_TX, frame.p1, frame.p2, frame.data, [StatusCodes.OK])
|
|
88
84
|
}
|
|
85
|
+
return decodeSignature(response!)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
function getTokenMetadata(unsignedTx: Buffer): TokenMetadata[] {
|
|
90
|
+
const result: TokenMetadata[] = []
|
|
91
|
+
const outputs = codec.unsignedTxCodec.decode(unsignedTx).fixedOutputs
|
|
92
|
+
outputs.forEach((output) => {
|
|
93
|
+
output.tokens.forEach((t) => {
|
|
94
|
+
const tokenIdHex = binToHex(t.tokenId)
|
|
95
|
+
if (result.find((t) => isTokenIdEqual(t.tokenId, tokenIdHex)) !== undefined) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
const metadata = merkleTokens.find((t) => isTokenIdEqual(t.tokenId, tokenIdHex))
|
|
99
|
+
if (metadata !== undefined && metadata.symbol.length <= MAX_TOKEN_SYMBOL_LENGTH) {
|
|
100
|
+
result.push(metadata)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
const size = Math.min(result.length, MAX_TOKEN_SIZE)
|
|
105
|
+
return result.slice(0, size)
|
|
106
|
+
}
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
function isTokenIdEqual(a: string, b: string): boolean {
|
|
109
|
+
return a.toLowerCase() === b.toLowerCase()
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
function decodeSignature(response: Buffer): string {
|
|
@@ -109,4 +117,4 @@ function decodeSignature(response: Buffer): string {
|
|
|
109
117
|
const s = response.slice(6 + rLen, 6 + rLen + sLen)
|
|
110
118
|
console.log(`${rLen} - ${r.toString('hex')}\n${sLen} - ${s.toString('hex')}`)
|
|
111
119
|
return encodeHexSignature(r.toString('hex'), s.toString('hex'))
|
|
112
|
-
}
|
|
120
|
+
}
|
package/src/merkle.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { binToHex, hexToBinUnsafe } from '@alephium/web3'
|
|
2
|
+
import mainnetTokenListJson from '../merkle-tree/token.json'
|
|
3
|
+
import proofsJson from '../merkle-tree/proofs.json'
|
|
4
|
+
import { serializeSingleTokenMetadata } from './serde'
|
|
5
|
+
import { TokenMetadata } from './types'
|
|
6
|
+
import { blake2b } from 'blakejs'
|
|
7
|
+
|
|
8
|
+
export const merkleTokens: TokenMetadata[] = mainnetTokenListJson.tokens.map((token) => {
|
|
9
|
+
return {
|
|
10
|
+
version: 0,
|
|
11
|
+
tokenId: token.id,
|
|
12
|
+
symbol: token.symbol,
|
|
13
|
+
decimals: token.decimals
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export function hashPair(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
18
|
+
return blake2b(Buffer.concat([a, b].sort(Buffer.compare)), undefined, 32)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function generateMerkleTree(tokens: TokenMetadata[]): Uint8Array[][] {
|
|
22
|
+
let level: Uint8Array[] = tokens.map((token) => blake2b(serializeSingleTokenMetadata(token), undefined, 32))
|
|
23
|
+
|
|
24
|
+
const tree: Uint8Array[][] = []
|
|
25
|
+
while (level.length > 1) {
|
|
26
|
+
tree.push(level)
|
|
27
|
+
level = level.reduce<Uint8Array[]>((acc, _, i, arr) => {
|
|
28
|
+
if (i % 2 === 0) {
|
|
29
|
+
acc.push(i + 1 < arr.length ? hashPair(arr[i], arr[i + 1]) : arr[i])
|
|
30
|
+
}
|
|
31
|
+
return acc
|
|
32
|
+
}, [])
|
|
33
|
+
}
|
|
34
|
+
tree.push(level) // Root
|
|
35
|
+
|
|
36
|
+
return tree
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function generateProofs(): { proofs: Record<string, string>; root: string } {
|
|
40
|
+
const tree = generateMerkleTree(merkleTokens)
|
|
41
|
+
const proofs = merkleTokens.reduce<Record<string, string>>((acc, token, tokenIndex) => {
|
|
42
|
+
const proof = tree.slice(0, -1).reduce<Uint8Array[]>((proofAcc, level, levelIndex) => {
|
|
43
|
+
const index = Math.floor(tokenIndex / 2 ** levelIndex)
|
|
44
|
+
const pairIndex = index % 2 === 0 ? index + 1 : index - 1
|
|
45
|
+
const siblingOrUncle = level[pairIndex]
|
|
46
|
+
|
|
47
|
+
if (siblingOrUncle) {
|
|
48
|
+
proofAcc.push(siblingOrUncle)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return proofAcc
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
acc[token.tokenId] = proof.map((hash) => binToHex(hash)).join('')
|
|
55
|
+
return acc
|
|
56
|
+
}, {})
|
|
57
|
+
|
|
58
|
+
console.log('root', tree[tree.length - 1].map((hash) => binToHex(hash)).join(''))
|
|
59
|
+
return { proofs, root: binToHex(tree[tree.length - 1][0]) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const tokenMerkleRoot = hexToBinUnsafe('b3380866c595544781e9da0ccd79399de8878abfb0bf40545b57a287387d419d')
|
|
63
|
+
export const tokenMerkleProofs = proofsJson as Record<string, string>
|
package/src/serde.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isHexString } from
|
|
2
|
-
import { MAX_TOKEN_SIZE, MAX_TOKEN_SYMBOL_LENGTH, TOKEN_METADATA_SIZE, TokenMetadata } from
|
|
1
|
+
import { isHexString } from '@alephium/web3'
|
|
2
|
+
import { MAX_TOKEN_SIZE, MAX_TOKEN_SYMBOL_LENGTH, TOKEN_METADATA_SIZE, TokenMetadata } from './types'
|
|
3
3
|
|
|
4
4
|
export const TRUE = 0x10
|
|
5
5
|
export const FALSE = 0x00
|
|
@@ -40,7 +40,7 @@ function symbolToBytes(symbol: string): Buffer {
|
|
|
40
40
|
return buffer
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function
|
|
43
|
+
export function checkTokenMetadata(tokens: TokenMetadata[]) {
|
|
44
44
|
const hasDuplicate = tokens.some((token, index) => index !== tokens.findIndex((t) => t.tokenId === token.tokenId))
|
|
45
45
|
if (hasDuplicate) {
|
|
46
46
|
throw new Error(`There are duplicate tokens`)
|
|
@@ -60,21 +60,22 @@ function check(tokens: TokenMetadata[]) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export function serializeSingleTokenMetadata(metadata: TokenMetadata): Buffer {
|
|
64
|
+
const symbolBytes = symbolToBytes(metadata.symbol)
|
|
65
|
+
const buffer = Buffer.concat([
|
|
66
|
+
Buffer.from([metadata.version]),
|
|
67
|
+
Buffer.from(metadata.tokenId, 'hex'),
|
|
68
|
+
symbolBytes,
|
|
69
|
+
Buffer.from([metadata.decimals]),
|
|
70
|
+
])
|
|
71
|
+
if (buffer.length !== TOKEN_METADATA_SIZE) {
|
|
72
|
+
throw new Error(`Invalid token metadata: ${metadata}`)
|
|
73
|
+
}
|
|
74
|
+
return buffer
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
export function serializeTokenMetadata(tokens: TokenMetadata[]): Buffer {
|
|
64
|
-
|
|
65
|
-
const array = tokens
|
|
66
|
-
.map((metadata) => {
|
|
67
|
-
const symbolBytes = symbolToBytes(metadata.symbol)
|
|
68
|
-
const buffer = Buffer.concat([
|
|
69
|
-
Buffer.from([metadata.version]),
|
|
70
|
-
Buffer.from(metadata.tokenId, 'hex'),
|
|
71
|
-
symbolBytes,
|
|
72
|
-
Buffer.from([metadata.decimals])
|
|
73
|
-
])
|
|
74
|
-
if (buffer.length !== TOKEN_METADATA_SIZE) {
|
|
75
|
-
throw new Error(`Invalid token metadata: ${metadata}`)
|
|
76
|
-
}
|
|
77
|
-
return buffer
|
|
78
|
-
})
|
|
78
|
+
checkTokenMetadata(tokens)
|
|
79
|
+
const array = tokens.map((metadata) => serializeSingleTokenMetadata(metadata))
|
|
79
80
|
return Buffer.concat([Buffer.from([array.length]), ...array])
|
|
80
81
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { tokenMerkleProofs } from "./merkle"
|
|
2
|
+
import { checkTokenMetadata, serializePath, serializeSingleTokenMetadata } from "./serde"
|
|
3
|
+
import { MAX_PAYLOAD_SIZE, TokenMetadata } from "./types"
|
|
4
|
+
|
|
5
|
+
export interface Frame {
|
|
6
|
+
p1: number
|
|
7
|
+
p2: number
|
|
8
|
+
data: Buffer
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function encodeTokenMetadata(tokenMetadata: TokenMetadata[]): Frame[] {
|
|
12
|
+
const frames = tokenMetadata.flatMap((metadata, index) => {
|
|
13
|
+
const isFirstToken = index === 0
|
|
14
|
+
const firstFramePrefix = isFirstToken ? Buffer.from([tokenMetadata.length]) : Buffer.alloc(0)
|
|
15
|
+
const buffers = encodeTokenAndProof(metadata, firstFramePrefix)
|
|
16
|
+
if (buffers.length === 0) return []
|
|
17
|
+
|
|
18
|
+
assert(buffers.every((buffer) => buffer.length <= MAX_PAYLOAD_SIZE), 'Invalid token frame size')
|
|
19
|
+
const frames: Frame[] = []
|
|
20
|
+
const firstFrameP2 = isFirstToken ? 0 : 1
|
|
21
|
+
frames.push({ p1: 0, p2: firstFrameP2, data: buffers[0] })
|
|
22
|
+
buffers.slice(1).forEach((data) => frames.push({ p1: 0, p2: 2, data }))
|
|
23
|
+
return frames
|
|
24
|
+
})
|
|
25
|
+
if (frames.length === 0) {
|
|
26
|
+
return [{ p1: 0, p2: 0, data: Buffer.from([0]) }]
|
|
27
|
+
} else {
|
|
28
|
+
return frames
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function encodeTokenAndProof(
|
|
33
|
+
tokenMetadata: TokenMetadata,
|
|
34
|
+
firstFramePrefix: Buffer
|
|
35
|
+
): Buffer[] {
|
|
36
|
+
const proof = tokenMerkleProofs[tokenMetadata.tokenId]
|
|
37
|
+
if (proof === undefined) return []
|
|
38
|
+
const proofBytes = Buffer.from(proof, 'hex')
|
|
39
|
+
const encodedProofLength = encodeProofLength(proofBytes.length)
|
|
40
|
+
const encodedTokenMetadata = serializeSingleTokenMetadata(tokenMetadata)
|
|
41
|
+
|
|
42
|
+
const firstFrameRemainSize =
|
|
43
|
+
MAX_PAYLOAD_SIZE - encodedTokenMetadata.length - encodedProofLength.length - firstFramePrefix.length
|
|
44
|
+
const firstFrameProofSize = Math.floor(firstFrameRemainSize / 32) * 32
|
|
45
|
+
if (firstFrameProofSize >= proofBytes.length) {
|
|
46
|
+
return [Buffer.concat([firstFramePrefix, encodedTokenMetadata, encodedProofLength, proofBytes])]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const firstFrameProof = proofBytes.slice(0, firstFrameProofSize)
|
|
50
|
+
const result: Buffer[] = [Buffer.concat([firstFramePrefix, encodedTokenMetadata, encodedProofLength, firstFrameProof])]
|
|
51
|
+
let from_index = firstFrameProofSize
|
|
52
|
+
while (from_index < proofBytes.length) {
|
|
53
|
+
const remainProofLength = proofBytes.length - from_index
|
|
54
|
+
const frameProofSize = Math.min(Math.floor(MAX_PAYLOAD_SIZE / 32) * 32, remainProofLength)
|
|
55
|
+
const frameProof = proofBytes.slice(from_index, from_index + frameProofSize)
|
|
56
|
+
from_index += frameProofSize
|
|
57
|
+
result.push(frameProof)
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function encodeProofLength(length: number): Uint8Array {
|
|
63
|
+
assert((length % 32 === 0) && (length > 0 && length < 0xffff), 'Invalid token proof size')
|
|
64
|
+
const buffer = Buffer.alloc(2);
|
|
65
|
+
buffer.writeUint16BE(length);
|
|
66
|
+
return buffer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function encodeUnsignedTx(path: string, unsignedTx: Buffer): Frame[] {
|
|
70
|
+
const encodedPath = serializePath(path)
|
|
71
|
+
const firstFrameTxLength = MAX_PAYLOAD_SIZE - 20;
|
|
72
|
+
if (firstFrameTxLength >= unsignedTx.length) {
|
|
73
|
+
return [{ p1: 1, p2: 0, data: Buffer.concat([encodedPath, unsignedTx]) }]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const firstFrameTxData = unsignedTx.slice(0, firstFrameTxLength)
|
|
77
|
+
const frames: Frame[] = [{ p1: 1, p2: 0, data: Buffer.concat([encodedPath, firstFrameTxData]) }]
|
|
78
|
+
let fromIndex = firstFrameTxLength
|
|
79
|
+
while (fromIndex < unsignedTx.length) {
|
|
80
|
+
const remain = unsignedTx.length - fromIndex
|
|
81
|
+
const frameTxLength = Math.min(MAX_PAYLOAD_SIZE, remain)
|
|
82
|
+
frames.push({ p1: 1, p2: 1, data: unsignedTx.slice(fromIndex, fromIndex + frameTxLength) })
|
|
83
|
+
fromIndex += frameTxLength
|
|
84
|
+
}
|
|
85
|
+
return frames
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function assert(condition: boolean, msg: string) {
|
|
89
|
+
if (!condition) throw Error(msg)
|
|
90
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export const MAX_TOKEN_SIZE = 5
|
|
2
2
|
export const MAX_TOKEN_SYMBOL_LENGTH = 12
|
|
3
3
|
export const TOKEN_METADATA_SIZE = 46
|
|
4
|
+
// The maximum payload size is 255: https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-transport/src/Transport.ts#L261
|
|
5
|
+
export const MAX_PAYLOAD_SIZE = 255
|
|
4
6
|
|
|
5
7
|
export interface TokenMetadata {
|
|
6
8
|
version: number,
|
|
7
9
|
tokenId: string,
|
|
8
10
|
symbol: string,
|
|
9
11
|
decimals: number
|
|
10
|
-
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { hashPair, merkleTokens, tokenMerkleProofs, tokenMerkleRoot } from '../src/merkle'
|
|
2
|
+
import { serializeSingleTokenMetadata } from '../src/serde'
|
|
3
|
+
import { blake2b } from 'blakejs'
|
|
4
|
+
import { binToHex } from '@alephium/web3'
|
|
5
|
+
|
|
6
|
+
describe('Merkle', () => {
|
|
7
|
+
it('should verify proofs', () => {
|
|
8
|
+
for (const token of merkleTokens) {
|
|
9
|
+
const proof = tokenMerkleProofs[token.tokenId]
|
|
10
|
+
|
|
11
|
+
let currentHash = blake2b(serializeSingleTokenMetadata(token), undefined, 32)
|
|
12
|
+
for (let i = 0; i < proof.length; i += 64) {
|
|
13
|
+
const sibling = proof.slice(i, i + 64)
|
|
14
|
+
currentHash = hashPair(currentHash, Buffer.from(sibling, 'hex'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
expect(JSON.stringify(currentHash)).toBe(JSON.stringify(tokenMerkleRoot))
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { merkleTokens, tokenMerkleProofs } from '../src/merkle'
|
|
2
|
+
import { assert, encodeProofLength, encodeTokenMetadata, encodeUnsignedTx } from '../src/tx-encoder'
|
|
3
|
+
import { MAX_PAYLOAD_SIZE, MAX_TOKEN_SIZE, TOKEN_METADATA_SIZE } from '../src'
|
|
4
|
+
import { serializePath, serializeSingleTokenMetadata } from '../src/serde';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
6
|
+
|
|
7
|
+
describe('TxEncoder', () => {
|
|
8
|
+
|
|
9
|
+
function shuffle<T>(array: T[]): T[] {
|
|
10
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
11
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
12
|
+
[array[i], array[j]] = [array[j], array[i]]
|
|
13
|
+
}
|
|
14
|
+
return array
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getFrameSize(proofLength: number): number {
|
|
18
|
+
if (proofLength <= 192) return 1
|
|
19
|
+
if (proofLength <= 416) return 2
|
|
20
|
+
if (proofLength <= 640) return 3
|
|
21
|
+
throw Error(`Invalid proof length: ${proofLength}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it('should encode token metadata and proof', () => {
|
|
25
|
+
const frames0 = encodeTokenMetadata([])
|
|
26
|
+
expect(frames0).toEqual([{ p1: 0, p2: 0, data: Buffer.from([0]) }])
|
|
27
|
+
|
|
28
|
+
const tokenSize = Math.floor(Math.random() * MAX_TOKEN_SIZE) + 1
|
|
29
|
+
assert(tokenSize >= 1 && tokenSize <= 5, 'Invalid token size')
|
|
30
|
+
const tokens = shuffle(Object.entries(tokenMerkleProofs))
|
|
31
|
+
const selectedTokens = tokens.slice(0, tokenSize)
|
|
32
|
+
const tokenMetadatas = selectedTokens.map(([tokenId]) => merkleTokens.find((t) => t.tokenId === tokenId)!)
|
|
33
|
+
const frames = encodeTokenMetadata(tokenMetadatas)
|
|
34
|
+
const tokenAndProofs = Buffer.concat(frames.map((frame, index) => index === 0 ? frame.data.slice(1) : frame.data))
|
|
35
|
+
|
|
36
|
+
const expected = Buffer.concat(tokenMetadatas.map((metadata, index) => {
|
|
37
|
+
const proof = Buffer.from(selectedTokens[index][1], 'hex')
|
|
38
|
+
const encodedProofLength = encodeProofLength(proof.length)
|
|
39
|
+
const encodedTokenMetadata = serializeSingleTokenMetadata(metadata)
|
|
40
|
+
return Buffer.concat([encodedTokenMetadata, encodedProofLength, proof])
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
expect(tokenAndProofs).toEqual(expected)
|
|
44
|
+
|
|
45
|
+
let frameIndex = 0
|
|
46
|
+
tokenMetadatas.forEach((_, index) => {
|
|
47
|
+
const proof = Buffer.from(selectedTokens[index][1], 'hex')
|
|
48
|
+
const isFirstToken = index === 0
|
|
49
|
+
const prefixLength = isFirstToken ? 1 + TOKEN_METADATA_SIZE + 2 : TOKEN_METADATA_SIZE + 2
|
|
50
|
+
const tokenFrames = frames.slice(frameIndex, frameIndex + getFrameSize(proof.length))
|
|
51
|
+
const firstFrameP2 = isFirstToken ? 0 : 1
|
|
52
|
+
expect(tokenFrames[0].p1).toEqual(0)
|
|
53
|
+
expect(tokenFrames[0].p2).toEqual(firstFrameP2)
|
|
54
|
+
tokenFrames.slice(1).forEach((frame) => {
|
|
55
|
+
expect(frame.p1).toEqual(0)
|
|
56
|
+
expect(frame.p2).toEqual(2)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const expectedProof = Buffer.concat([tokenFrames[0].data.slice(prefixLength), ...tokenFrames.slice(1).map((f) => f.data)])
|
|
60
|
+
expect(proof).toEqual(expectedProof)
|
|
61
|
+
|
|
62
|
+
frameIndex += tokenFrames.length
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should encode tx', () => {
|
|
67
|
+
const path = `m/44'/1234'/0'/0/0`
|
|
68
|
+
const encodedPath = serializePath(path)
|
|
69
|
+
const unsignedTx0 = randomBytes(200)
|
|
70
|
+
const frames0 = encodeUnsignedTx(path, unsignedTx0)
|
|
71
|
+
expect(frames0).toEqual([{ p1: 1, p2: 0, data: Buffer.concat([encodedPath, unsignedTx0]) }])
|
|
72
|
+
|
|
73
|
+
const unsignedTx1 = randomBytes(250)
|
|
74
|
+
const frames1 = encodeUnsignedTx(path, unsignedTx1)
|
|
75
|
+
expect(frames1).toEqual([
|
|
76
|
+
{ p1: 1, p2: 0, data: Buffer.concat([encodedPath, unsignedTx1.slice(0, MAX_PAYLOAD_SIZE - 20)]) },
|
|
77
|
+
{ p1: 1, p2: 1, data: unsignedTx1.slice( MAX_PAYLOAD_SIZE - 20) },
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
})
|
package/test/utils.ts
CHANGED
|
@@ -27,6 +27,8 @@ function getModel(): string {
|
|
|
27
27
|
export enum OutputType {
|
|
28
28
|
Base,
|
|
29
29
|
Multisig,
|
|
30
|
+
Nanos10,
|
|
31
|
+
Nanos11,
|
|
30
32
|
Token,
|
|
31
33
|
BaseAndToken,
|
|
32
34
|
MultisigAndToken
|
|
@@ -35,6 +37,8 @@ export enum OutputType {
|
|
|
35
37
|
const NanosClickTable = new Map([
|
|
36
38
|
[OutputType.Base, 5],
|
|
37
39
|
[OutputType.Multisig, 10],
|
|
40
|
+
[OutputType.Nanos10, 10],
|
|
41
|
+
[OutputType.Nanos11, 11],
|
|
38
42
|
[OutputType.Token, 11],
|
|
39
43
|
[OutputType.BaseAndToken, 12],
|
|
40
44
|
[OutputType.MultisigAndToken, 16],
|
|
@@ -139,6 +143,7 @@ async function touch(outputs: OutputType[], hasExternalInputs: boolean) {
|
|
|
139
143
|
|
|
140
144
|
export async function approveTx(outputs: OutputType[], hasExternalInputs: boolean = false) {
|
|
141
145
|
if (!needToAutoApprove()) return
|
|
146
|
+
await sleep(2000)
|
|
142
147
|
const isSelfTransfer = outputs.length === 0 && !hasExternalInputs
|
|
143
148
|
if (isSelfTransfer) {
|
|
144
149
|
if (isStaxOrFlex()) {
|
|
@@ -184,6 +189,10 @@ function isStaxOrFlex(): boolean {
|
|
|
184
189
|
return !getModel().startsWith('nano')
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
export function isNanos(): boolean {
|
|
193
|
+
return getModel() === 'nanos'
|
|
194
|
+
}
|
|
195
|
+
|
|
187
196
|
export function skipBlindSigningWarning() {
|
|
188
197
|
if (!needToAutoApprove()) return
|
|
189
198
|
if (isStaxOrFlex()) {
|