@alephium/ledger-app 0.5.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alephium/ledger-app",
3
- "version": "0.5.2",
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.2.0",
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/web3-test": "^1.2.0",
34
- "@alephium/web3-wallet": "^1.2.0",
35
- "@alephium/cli": "^1.2.0",
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,9 +19,6 @@ export enum INS {
17
19
  export const GROUP_NUM = 4
18
20
  export const HASH_LEN = 32
19
21
 
20
- // The maximum payload size is 255: https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-transport/src/Transport.ts#L261
21
- const MAX_PAYLOAD_SIZE = 255
22
-
23
22
  export class AlephiumApp {
24
23
  readonly transport: Transport
25
24
 
@@ -71,34 +70,43 @@ export 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 encodedPath = serde.serializePath(path)
81
- const encodedTokenMetadata = serde.serializeTokenMetadata(tokenMetadata)
82
- const firstFrameTxLength = MAX_PAYLOAD_SIZE - 20 - encodedTokenMetadata.length;
83
- const txData = unsignedTx.slice(0, unsignedTx.length > firstFrameTxLength ? firstFrameTxLength : unsignedTx.length)
84
- const data = Buffer.concat([encodedPath, encodedTokenMetadata, txData])
85
- let response = await this.transport.send(CLA, INS.SIGN_TX, 0x00, 0x00, data, [StatusCodes.OK])
86
- if (unsignedTx.length <= firstFrameTxLength) {
87
- return decodeSignature(response)
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
- const frameLength = MAX_PAYLOAD_SIZE
91
- let fromIndex = firstFrameTxLength
92
- while (fromIndex < unsignedTx.length) {
93
- const remain = unsignedTx.length - fromIndex
94
- const toIndex = remain > frameLength ? (fromIndex + frameLength) : unsignedTx.length
95
- const data = unsignedTx.slice(fromIndex, toIndex)
96
- response = await this.transport.send(CLA, INS.SIGN_TX, 0x01, 0x00, data, [StatusCodes.OK])
97
- fromIndex = toIndex
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
- return decodeSignature(response)
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 "@alephium/web3"
2
- import { MAX_TOKEN_SIZE, MAX_TOKEN_SYMBOL_LENGTH, TOKEN_METADATA_SIZE, TokenMetadata } from "./types"
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 check(tokens: TokenMetadata[]) {
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
- check(tokens)
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()) {
@@ -3,9 +3,10 @@ import { ALPH_TOKEN_ID, Address, DUST_AMOUNT, NodeProvider, ONE_ALPH, binToHex,
3
3
  import { getSigner, mintToken, transfer } from '@alephium/web3-test'
4
4
  import { PrivateKeyWallet } from '@alephium/web3-wallet'
5
5
  import blake from 'blakejs'
6
- import { approveAddress, approveHash, approveTx, createTransport, enableBlindSigning, getRandomInt, needToAutoApprove, OutputType, skipBlindSigningWarning, staxFlexApproveOnce } from './utils'
6
+ import { approveAddress, approveHash, approveTx, createTransport, enableBlindSigning, getRandomInt, isNanos, needToAutoApprove, OutputType, skipBlindSigningWarning, staxFlexApproveOnce } from './utils'
7
7
  import { TokenMetadata } from '../src/types'
8
- import { randomInt } from 'crypto'
8
+ import { randomBytes } from 'crypto'
9
+ import { merkleTokens, tokenMerkleProofs } from '../src/merkle'
9
10
 
10
11
  describe('ledger wallet', () => {
11
12
  const nodeProvider = new NodeProvider("http://127.0.0.1:22973")
@@ -272,37 +273,87 @@ describe('ledger wallet', () => {
272
273
  return { tokens, destinations }
273
274
  }
274
275
 
275
- it('should transfer token with metadata', async () => {
276
+ it('should transfer tokens with proof', async () => {
276
277
  const transport = await createTransport()
277
278
  const app = new AlephiumApp(transport)
278
279
  const [testAccount] = await app.getAccount(path)
279
280
  await transferToAddress(testAccount.address)
281
+ const newAccount = await getSigner()
282
+
283
+ const selectedTokens = [
284
+ merkleTokens[5], // decimals is 0
285
+ merkleTokens[6], // decimals is 18
286
+ merkleTokens[8], // decimals is 9
287
+ merkleTokens[11], // decimals is 8
288
+ merkleTokens[13], // decimals is 6
289
+ ]
290
+ const outputs: node.FixedAssetOutput[] = selectedTokens.map((token, index) => {
291
+ return {
292
+ hint:0,
293
+ key: '',
294
+ attoAlphAmount: DUST_AMOUNT.toString(),
295
+ address: newAccount.address,
296
+ tokens: [{ id: token.tokenId, amount: (BigInt(index + 1) * ONE_ALPH).toString() }],
297
+ lockTime: 0,
298
+ message: ''
299
+ }
300
+ })
301
+ const unsignedTx: node.UnsignedTx = {
302
+ txId: '',
303
+ version: 0,
304
+ networkId: 4,
305
+ gasAmount: 100000,
306
+ gasPrice: (ONE_ALPH / 10000000n).toString(),
307
+ inputs: [{ outputRef: { hint: 0, key: binToHex(randomBytes(32)) }, unlockScript: '00' + testAccount.publicKey }],
308
+ fixedOutputs: outputs
309
+ }
310
+ const encodedUnsignedTx = codec.unsignedTxCodec.encodeApiUnsignedTx(unsignedTx)
280
311
 
281
- const toAddress = '1BmVCLrjttchZMW7i6df7mTdCKzHpy38bgDbVL1GqV6P7';
282
- const transferAmount = 1234567890123456789012345n
283
- const mintAmount = 2222222222222222222222222n
284
- const { tokens, destinations } = await genTokensAndDestinations(testAccount.address, toAddress, mintAmount, transferAmount)
312
+ if (isNanos()) {
313
+ approveTx([OutputType.Nanos11, OutputType.Nanos10, OutputType.Nanos10, OutputType.Nanos10, OutputType.Nanos11])
314
+ } else {
315
+ approveTx(Array(5).fill(OutputType.BaseAndToken))
316
+ }
317
+ const signature = await app.signUnsignedTx(path, Buffer.from(encodedUnsignedTx))
318
+ const txId = blake.blake2b(encodedUnsignedTx, undefined, 32)
319
+ expect(transactionVerifySignature(binToHex(txId), testAccount.publicKey, signature)).toBe(true)
285
320
 
286
- const randomOrderTokens = tokens.sort((a, b) => b.tokenId.localeCompare(a.tokenId))
287
- const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
288
- fromPublicKey: testAccount.publicKey,
289
- destinations: destinations
290
- })
321
+ await app.close()
322
+ }, 120000)
291
323
 
292
- approveTx(Array(5).fill(OutputType.BaseAndToken))
293
- const signature = await app.signUnsignedTx(path, Buffer.from(buildTxResult.unsignedTx, 'hex'), randomOrderTokens)
294
- expect(transactionVerifySignature(buildTxResult.txId, testAccount.publicKey, signature)).toBe(true)
324
+ it('should reject tx if the token proof is invalid', async () => {
325
+ const transport = await createTransport()
326
+ const app = new AlephiumApp(transport)
327
+ const [testAccount] = await app.getAccount(path)
328
+ await transferToAddress(testAccount.address)
295
329
 
296
- const submitResult = await nodeProvider.transactions.postTransactionsSubmit({
297
- unsignedTx: buildTxResult.unsignedTx,
298
- signature: signature
299
- })
300
- await waitForTxConfirmation(submitResult.txId, 1, 1000)
301
- const balances = await nodeProvider.addresses.getAddressesAddressBalance(toAddress)
302
- tokens.forEach((metadata) => {
303
- const tokenBalance = balances.tokenBalances!.find((t) => t.id === metadata.tokenId)!
304
- expect(BigInt(tokenBalance.amount)).toEqual(transferAmount)
305
- })
330
+ const toAddress = '1BmVCLrjttchZMW7i6df7mTdCKzHpy38bgDbVL1GqV6P7'
331
+ const selectedToken = merkleTokens[6] // the decimals is 18
332
+ const output: node.FixedAssetOutput = {
333
+ hint: 0,
334
+ key: '',
335
+ attoAlphAmount: DUST_AMOUNT.toString(),
336
+ address: toAddress,
337
+ tokens: [{ id: selectedToken.tokenId, amount: ONE_ALPH.toString() }],
338
+ lockTime: 0,
339
+ message: ''
340
+ }
341
+ const unsignedTx: node.UnsignedTx = {
342
+ txId: '',
343
+ version: 0,
344
+ networkId: 4,
345
+ gasAmount: 100000,
346
+ gasPrice: (ONE_ALPH / 10000000n).toString(),
347
+ inputs: [{ outputRef: { hint: 0, key: binToHex(randomBytes(32)) }, unlockScript: '00' + testAccount.publicKey }],
348
+ fixedOutputs: [output]
349
+ }
350
+ const encodedUnsignedTx = codec.unsignedTxCodec.encodeApiUnsignedTx(unsignedTx)
351
+
352
+ const originalProof = tokenMerkleProofs[selectedToken.tokenId]
353
+ const invalidProof = originalProof.slice(0, originalProof.length - 64)
354
+ tokenMerkleProofs[selectedToken.tokenId] = invalidProof
355
+ await expect(app.signUnsignedTx(path, Buffer.from(encodedUnsignedTx))).rejects.toThrow()
356
+ tokenMerkleProofs[selectedToken.tokenId] = originalProof
306
357
 
307
358
  await app.close()
308
359
  }, 120000)
@@ -313,19 +364,31 @@ describe('ledger wallet', () => {
313
364
  const [testAccount] = await app.getAccount(path)
314
365
  await transferToAddress(testAccount.address)
315
366
 
316
- const toAddress = '1BmVCLrjttchZMW7i6df7mTdCKzHpy38bgDbVL1GqV6P7';
317
- const transferAmount = 1234567890123456789012345n
318
- const mintAmount = 2222222222222222222222222n
319
- const { tokens, destinations } = await genTokensAndDestinations(testAccount.address, toAddress, mintAmount, transferAmount)
320
-
321
- const invalidTokenIndex = randomInt(5)
322
- tokens[invalidTokenIndex] = { ...tokens[invalidTokenIndex], version: 1 }
323
- const buildTxResult = await nodeProvider.transactions.postTransactionsBuild({
324
- fromPublicKey: testAccount.publicKey,
325
- destinations: destinations
326
- })
327
-
328
- await expect(app.signUnsignedTx(path, Buffer.from(buildTxResult.unsignedTx, 'hex'), tokens)).rejects.toThrow()
367
+ const toAddress = '1BmVCLrjttchZMW7i6df7mTdCKzHpy38bgDbVL1GqV6P7'
368
+ const tokenIndex = 6
369
+ const selectedToken = merkleTokens[tokenIndex]
370
+ const output: node.FixedAssetOutput = {
371
+ hint: 0,
372
+ key: '',
373
+ attoAlphAmount: DUST_AMOUNT.toString(),
374
+ address: toAddress,
375
+ tokens: [{ id: selectedToken.tokenId, amount: ONE_ALPH.toString() }],
376
+ lockTime: 0,
377
+ message: ''
378
+ }
379
+ const unsignedTx: node.UnsignedTx = {
380
+ txId: '',
381
+ version: 0,
382
+ networkId: 4,
383
+ gasAmount: 100000,
384
+ gasPrice: (ONE_ALPH / 10000000n).toString(),
385
+ inputs: [{ outputRef: { hint: 0, key: binToHex(randomBytes(32)) }, unlockScript: '00' + testAccount.publicKey }],
386
+ fixedOutputs: [output]
387
+ }
388
+ const encodedUnsignedTx = codec.unsignedTxCodec.encodeApiUnsignedTx(unsignedTx)
389
+ merkleTokens[tokenIndex] = { ...selectedToken, version: 1 }
390
+ await expect(app.signUnsignedTx(path, Buffer.from(encodedUnsignedTx))).rejects.toThrow()
391
+ merkleTokens[tokenIndex] = selectedToken
329
392
 
330
393
  await app.close()
331
394
  }, 120000)