@sign-global/tokentable-wallets 1.0.1 → 1.2.1
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/CHANGELOG.md +13 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +380 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +393 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/src/providers/solana/index.tsx +49 -29
- package/src/types.ts +3 -1
- package/src/utils/ledger/adapter.ts +227 -0
- package/src/utils/ledger/index.ts +2 -0
- package/src/utils/ledger/polyfills/Buffer.ts +7 -0
- package/src/utils/ledger/polyfills/index.ts +1 -0
- package/src/utils/ledger/util.ts +252 -0
- package/tsup.config.ts +2 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { StatusCodes, TransportStatusError, type default as Transport } from '@ledgerhq/hw-transport';
|
|
2
|
+
import { isVersionedTransaction } from '@solana/wallet-adapter-base';
|
|
3
|
+
import { PublicKey, type Transaction, type VersionedTransaction } from '@solana/web3.js';
|
|
4
|
+
import './polyfills/index.js';
|
|
5
|
+
|
|
6
|
+
export function getDerivationPath(account?: number, change?: number): Buffer {
|
|
7
|
+
const length = account !== undefined ? (change === undefined ? 3 : 4) : 2;
|
|
8
|
+
const derivationPath = Buffer.alloc(1 + length * 4);
|
|
9
|
+
|
|
10
|
+
let offset = derivationPath.writeUInt8(length, 0);
|
|
11
|
+
offset = derivationPath.writeUInt32BE(harden(44), offset); // Using BIP44
|
|
12
|
+
offset = derivationPath.writeUInt32BE(harden(501), offset); // Solana's BIP44 path
|
|
13
|
+
|
|
14
|
+
if (account !== undefined) {
|
|
15
|
+
offset = derivationPath.writeUInt32BE(harden(account), offset);
|
|
16
|
+
if (change !== undefined) {
|
|
17
|
+
derivationPath.writeUInt32BE(harden(change), offset);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return derivationPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const BIP32_HARDENED_BIT = (1 << 31) >>> 0;
|
|
25
|
+
|
|
26
|
+
function harden(n: number): number {
|
|
27
|
+
return (n | BIP32_HARDENED_BIT) >>> 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const INS_GET_VERSION = 0x04;
|
|
31
|
+
const INS_GET_PUBKEY = 0x05;
|
|
32
|
+
const INS_SIGN_MESSAGE = 0x06;
|
|
33
|
+
const INS_SIGN_MESSAGE_OFFCHAIN = 0x07;
|
|
34
|
+
|
|
35
|
+
const P1_NON_CONFIRM = 0x00;
|
|
36
|
+
const P1_CONFIRM = 0x01;
|
|
37
|
+
|
|
38
|
+
const P2_EXTEND = 0x01;
|
|
39
|
+
const P2_MORE = 0x02;
|
|
40
|
+
|
|
41
|
+
const MAX_PAYLOAD = 255;
|
|
42
|
+
|
|
43
|
+
const LEDGER_CLA = 0xe0;
|
|
44
|
+
|
|
45
|
+
// Max off-chain message length supported by Ledger
|
|
46
|
+
const OFFCM_MAX_LEDGER_LEN = 1212;
|
|
47
|
+
// Max length of version 0 off-chain message
|
|
48
|
+
const OFFCM_MAX_V0_LEN = 65515;
|
|
49
|
+
|
|
50
|
+
export class OffchainMessage {
|
|
51
|
+
version: number;
|
|
52
|
+
messageFormat: number | undefined;
|
|
53
|
+
message: Buffer | undefined;
|
|
54
|
+
signerAddress: PublicKey;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Constructs a new OffchainMessage
|
|
58
|
+
* @param {version: number, messageFormat: number, message: string | Buffer} opts - Constructor parameters
|
|
59
|
+
*/
|
|
60
|
+
constructor(opts: { version?: number; messageFormat?: number; message: Buffer; signerAddress: PublicKey }) {
|
|
61
|
+
this.version = 0;
|
|
62
|
+
this.messageFormat = undefined;
|
|
63
|
+
this.message = undefined;
|
|
64
|
+
this.signerAddress = opts.signerAddress;
|
|
65
|
+
|
|
66
|
+
if (!opts) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (opts.version) {
|
|
70
|
+
this.version = opts.version;
|
|
71
|
+
}
|
|
72
|
+
if (opts.messageFormat) {
|
|
73
|
+
this.messageFormat = opts.messageFormat;
|
|
74
|
+
}
|
|
75
|
+
if (opts.message) {
|
|
76
|
+
this.message = Buffer.from(opts.message);
|
|
77
|
+
if (this.version === 0) {
|
|
78
|
+
if (!this.messageFormat) {
|
|
79
|
+
this.messageFormat = OffchainMessage.guessMessageFormat(this.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static guessMessageFormat(message: Buffer) {
|
|
86
|
+
if (Object.prototype.toString.call(message) !== '[object Uint8Array]') {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
if (message.length <= OFFCM_MAX_LEDGER_LEN) {
|
|
90
|
+
if (OffchainMessage.isPrintableASCII(message)) {
|
|
91
|
+
return 0;
|
|
92
|
+
} else if (OffchainMessage.isUTF8(message)) {
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
} else if (message.length <= OFFCM_MAX_V0_LEN) {
|
|
96
|
+
if (OffchainMessage.isUTF8(message)) {
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static isPrintableASCII(buffer: Buffer) {
|
|
104
|
+
return (
|
|
105
|
+
buffer &&
|
|
106
|
+
buffer.every((element) => {
|
|
107
|
+
return element == 0xa || (element >= 0x20 && element <= 0x7e);
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static isUTF8(buffer: Buffer) {
|
|
113
|
+
try {
|
|
114
|
+
new TextDecoder('utf8', { fatal: true }).decode(buffer);
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
isValid() {
|
|
122
|
+
if (this.version !== 0) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (!this.message) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const format = OffchainMessage.guessMessageFormat(this.message);
|
|
129
|
+
return format != null && format === this.messageFormat;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
isLedgerSupported(allowBlindSigning: boolean) {
|
|
133
|
+
return this.isValid() && (this.messageFormat === 0 || (this.messageFormat === 1 && allowBlindSigning));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
serialize() {
|
|
137
|
+
if (!this.isValid()) {
|
|
138
|
+
throw new Error(`Invalid OffchainMessage: ${JSON.stringify(this)}`);
|
|
139
|
+
}
|
|
140
|
+
const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from('solana offchain')]);
|
|
141
|
+
const headerVersion = Buffer.alloc(1);
|
|
142
|
+
const applicationDomain = Buffer.alloc(32);
|
|
143
|
+
const messageFormat = Buffer.alloc(1);
|
|
144
|
+
const signerCount = Buffer.alloc(1);
|
|
145
|
+
signerCount.writeUInt8(1);
|
|
146
|
+
|
|
147
|
+
const messageLength = Buffer.alloc(2);
|
|
148
|
+
|
|
149
|
+
// isValid() checks message
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
151
|
+
const messageBuffer = Buffer.from(this.message!);
|
|
152
|
+
|
|
153
|
+
// isValid() checks messageFormat
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
155
|
+
messageFormat.writeUInt8(this.messageFormat!);
|
|
156
|
+
|
|
157
|
+
const signers = this.signerAddress.toBuffer();
|
|
158
|
+
messageLength.writeUInt16LE(messageBuffer.length);
|
|
159
|
+
|
|
160
|
+
return Buffer.concat([
|
|
161
|
+
signingDomain,
|
|
162
|
+
headerVersion,
|
|
163
|
+
applicationDomain,
|
|
164
|
+
messageFormat,
|
|
165
|
+
signerCount,
|
|
166
|
+
signers,
|
|
167
|
+
messageLength,
|
|
168
|
+
messageBuffer
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @internal */
|
|
174
|
+
export async function getPublicKey(transport: Transport, derivationPath: Buffer): Promise<PublicKey> {
|
|
175
|
+
const bytes = await send(transport, INS_GET_PUBKEY, P1_NON_CONFIRM, derivationPath);
|
|
176
|
+
return new PublicKey(bytes);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @internal */
|
|
180
|
+
export async function signTransaction(
|
|
181
|
+
transport: Transport,
|
|
182
|
+
transaction: Transaction | VersionedTransaction,
|
|
183
|
+
derivationPath: Buffer
|
|
184
|
+
): Promise<Buffer> {
|
|
185
|
+
const paths = Buffer.alloc(1);
|
|
186
|
+
paths.writeUInt8(1, 0);
|
|
187
|
+
|
|
188
|
+
const message = isVersionedTransaction(transaction)
|
|
189
|
+
? transaction.message.serialize()
|
|
190
|
+
: transaction.serializeMessage();
|
|
191
|
+
const data = Buffer.concat([paths, derivationPath, message]);
|
|
192
|
+
|
|
193
|
+
return await send(transport, INS_SIGN_MESSAGE, P1_CONFIRM, data);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @internal */
|
|
197
|
+
export async function signMessage(transport: Transport, message: Buffer, derivationPath: Buffer): Promise<Buffer> {
|
|
198
|
+
const paths = Buffer.alloc(1);
|
|
199
|
+
paths.writeUInt8(1, 0);
|
|
200
|
+
|
|
201
|
+
const data = Buffer.concat([paths, derivationPath, message]);
|
|
202
|
+
|
|
203
|
+
return await send(transport, INS_SIGN_MESSAGE_OFFCHAIN, P1_CONFIRM, data);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @internal */
|
|
207
|
+
export async function getAppConfiguration(transport: Transport): Promise<AppConfig> {
|
|
208
|
+
const [blindSigningEnabled, pubKeyDisplayMode, major, minor, patch] = await send(
|
|
209
|
+
transport,
|
|
210
|
+
INS_GET_VERSION,
|
|
211
|
+
P1_NON_CONFIRM,
|
|
212
|
+
Buffer.alloc(0)
|
|
213
|
+
);
|
|
214
|
+
return {
|
|
215
|
+
blindSigningEnabled: Boolean(blindSigningEnabled),
|
|
216
|
+
pubKeyDisplayMode,
|
|
217
|
+
version: `${major}.${minor}.${patch}`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
enum PubKeyDisplayMode {
|
|
222
|
+
LONG,
|
|
223
|
+
SHORT
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
type AppConfig = {
|
|
227
|
+
blindSigningEnabled: boolean;
|
|
228
|
+
pubKeyDisplayMode: PubKeyDisplayMode;
|
|
229
|
+
version: string;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
async function send(transport: Transport, instruction: number, p1: number, data: Buffer): Promise<Buffer> {
|
|
233
|
+
let p2 = 0;
|
|
234
|
+
let offset = 0;
|
|
235
|
+
|
|
236
|
+
if (data.length > MAX_PAYLOAD) {
|
|
237
|
+
while (data.length - offset > MAX_PAYLOAD) {
|
|
238
|
+
const buffer = data.slice(offset, offset + MAX_PAYLOAD);
|
|
239
|
+
const response = await transport.send(LEDGER_CLA, instruction, p1, p2 | P2_MORE, buffer);
|
|
240
|
+
|
|
241
|
+
if (response.length !== 2) throw new TransportStatusError(StatusCodes.INCORRECT_DATA);
|
|
242
|
+
|
|
243
|
+
p2 |= P2_EXTEND;
|
|
244
|
+
offset += MAX_PAYLOAD;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const buffer = data.slice(offset);
|
|
249
|
+
const response = await transport.send(LEDGER_CLA, instruction, p1, p2, buffer);
|
|
250
|
+
|
|
251
|
+
return response.slice(0, response.length - 2);
|
|
252
|
+
}
|