@emblemvault/primitives-stake 0.1.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/LICENSE +190 -0
- package/README.md +26 -0
- package/dist/client.d.ts +257 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +749 -0
- package/dist/client.js.map +1 -0
- package/dist/codec.d.ts +90 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +565 -0
- package/dist/codec.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/pda.d.ts +49 -0
- package/dist/pda.d.ts.map +1 -0
- package/dist/pda.js +58 -0
- package/dist/pda.js.map +1 -0
- package/dist/types.d.ts +151 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmblemStake — TS client for the stake program.
|
|
3
|
+
*
|
|
4
|
+
* Tier-1 surface: a developer outside Emblem:build runs `npm install
|
|
5
|
+
* @emblemvault/primitives-stake @emblemvault/auth-sdk`, hands the SDK an
|
|
6
|
+
* auth-sdk signer, creates pools / stakes / claims rewards.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1+ status:
|
|
9
|
+
* - PDA derivation, getPool/getVault/getDistribution — work today
|
|
10
|
+
* against any deployed program (read paths)
|
|
11
|
+
* - evaluateStakeGate — works today: takes a StakeVaultAccount + minimum,
|
|
12
|
+
* returns granted/lockExpired. This is what sdk/gate's stake-mode
|
|
13
|
+
* evaluator will call via the gate program's CPI; the off-chain version
|
|
14
|
+
* is useful for previews and tests
|
|
15
|
+
* - createPool / stake / unstake / claim / distributeRewards — stub
|
|
16
|
+
* NotYetImplemented until programs/stake ships (Phase 3)
|
|
17
|
+
*/
|
|
18
|
+
import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, Transaction, TransactionInstruction, } from '@solana/web3.js';
|
|
19
|
+
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
20
|
+
import { EmblemError, } from '@emblemvault/primitives-shared';
|
|
21
|
+
import { decodeRewardDistributionAccount, decodeStakePoolAccount, decodeStakeVaultAccount, MULTIPLIER_SCALE, PENALTY_TAG_BURN, PENALTY_TAG_REDISTRIBUTE, PENALTY_TAG_TREASURY_PDA, REWARD_CADENCE_DAILY, REWARD_CADENCE_HOURLY, REWARD_CADENCE_WEEKLY, REWARD_FORMULA_LINEAR_V1, REWARD_FORMULA_SAMMY_V1, REWARD_SOURCE_TAG_DIST_VAULT, REWARD_SOURCE_TAG_SPLIT_RECIPIENT, STAKE_POOL_DISCRIMINATOR, STAKE_VAULT_DISCRIMINATOR, } from './codec.js';
|
|
22
|
+
import { deriveRewardDistPda, deriveStakePoolPda, deriveStakeVaultPda, STAKE_PROGRAM_ID, } from './pda.js';
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Anchor instruction discriminators
|
|
25
|
+
// =============================================================================
|
|
26
|
+
const IX_CREATE_POOL = Buffer.from([
|
|
27
|
+
0xe9, 0x92, 0xd1, 0x8e, 0xcf, 0x68, 0x40, 0xbc,
|
|
28
|
+
]);
|
|
29
|
+
const IX_REGISTER_REWARD_VAULT = Buffer.from([
|
|
30
|
+
0xcb, 0x37, 0x29, 0x9c, 0xfc, 0x7f, 0xb9, 0xef,
|
|
31
|
+
]);
|
|
32
|
+
const IX_REGISTER_PENDING_ACTION_VAULT = Buffer.from([
|
|
33
|
+
0x5b, 0x60, 0x22, 0xa1, 0x04, 0x7c, 0xa3, 0x62,
|
|
34
|
+
]);
|
|
35
|
+
const IX_REGISTER_POOL_TOKEN_ACCOUNTS = Buffer.from([
|
|
36
|
+
0x3c, 0xd3, 0x00, 0x47, 0x80, 0x7b, 0xc8, 0x52,
|
|
37
|
+
]);
|
|
38
|
+
const IX_STAKE = Buffer.from([
|
|
39
|
+
0xce, 0xb0, 0xca, 0x12, 0xc8, 0xd1, 0xb3, 0x6c,
|
|
40
|
+
]);
|
|
41
|
+
const IX_UNSTAKE = Buffer.from([
|
|
42
|
+
0x5a, 0x5f, 0x6b, 0x2a, 0xcd, 0x7c, 0x32, 0xe1,
|
|
43
|
+
]);
|
|
44
|
+
const IX_CLAIM = Buffer.from([
|
|
45
|
+
0x3e, 0xc6, 0xd6, 0xc1, 0xd5, 0x9f, 0x6c, 0xd2,
|
|
46
|
+
]);
|
|
47
|
+
const IX_DISTRIBUTE_REWARDS = Buffer.from([
|
|
48
|
+
0x61, 0x06, 0xe3, 0xff, 0x7c, 0xa5, 0x03, 0x94,
|
|
49
|
+
]);
|
|
50
|
+
const ACTION_TYPE_BUYBACK_BURN = 0;
|
|
51
|
+
const ACTION_TYPE_LP_REINFORCE = 1;
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Borsh helpers
|
|
54
|
+
// =============================================================================
|
|
55
|
+
function bsString(s) {
|
|
56
|
+
const utf8 = Buffer.from(s, 'utf-8');
|
|
57
|
+
const len = Buffer.alloc(4);
|
|
58
|
+
len.writeUInt32LE(utf8.length, 0);
|
|
59
|
+
return Buffer.concat([len, utf8]);
|
|
60
|
+
}
|
|
61
|
+
function bsOptString(s) {
|
|
62
|
+
if (s == null)
|
|
63
|
+
return Buffer.from([0]);
|
|
64
|
+
return Buffer.concat([Buffer.from([1]), bsString(s)]);
|
|
65
|
+
}
|
|
66
|
+
function bsOptU64(v) {
|
|
67
|
+
if (v == null)
|
|
68
|
+
return Buffer.from([0]);
|
|
69
|
+
const buf = Buffer.alloc(9);
|
|
70
|
+
buf.writeUInt8(1, 0);
|
|
71
|
+
buf.writeBigUInt64LE(v, 1);
|
|
72
|
+
return buf;
|
|
73
|
+
}
|
|
74
|
+
function u32(n) {
|
|
75
|
+
const b = Buffer.alloc(4);
|
|
76
|
+
b.writeUInt32LE(n, 0);
|
|
77
|
+
return b;
|
|
78
|
+
}
|
|
79
|
+
function u16(n) {
|
|
80
|
+
const b = Buffer.alloc(2);
|
|
81
|
+
b.writeUInt16LE(n, 0);
|
|
82
|
+
return b;
|
|
83
|
+
}
|
|
84
|
+
function u64(n) {
|
|
85
|
+
const b = Buffer.alloc(8);
|
|
86
|
+
b.writeBigUInt64LE(n, 0);
|
|
87
|
+
return b;
|
|
88
|
+
}
|
|
89
|
+
function encodePenaltyDestination(p) {
|
|
90
|
+
switch (p.kind) {
|
|
91
|
+
case 'redistribute':
|
|
92
|
+
return Buffer.from([PENALTY_TAG_REDISTRIBUTE]);
|
|
93
|
+
case 'burn':
|
|
94
|
+
return Buffer.from([PENALTY_TAG_BURN]);
|
|
95
|
+
case 'treasury-pda':
|
|
96
|
+
return Buffer.concat([Buffer.from([PENALTY_TAG_TREASURY_PDA]), bsString(p.id)]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function encodeRewardSource(s) {
|
|
100
|
+
if (s.type === 'distribution-vault') {
|
|
101
|
+
return Buffer.concat([Buffer.from([REWARD_SOURCE_TAG_DIST_VAULT]), bsString(s.id)]);
|
|
102
|
+
}
|
|
103
|
+
return Buffer.concat([
|
|
104
|
+
Buffer.from([REWARD_SOURCE_TAG_SPLIT_RECIPIENT]),
|
|
105
|
+
bsString(s.fromConfigId),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
function encodeRewardCadence(c) {
|
|
109
|
+
if (c === 'hourly')
|
|
110
|
+
return REWARD_CADENCE_HOURLY;
|
|
111
|
+
if (c === 'daily')
|
|
112
|
+
return REWARD_CADENCE_DAILY;
|
|
113
|
+
return REWARD_CADENCE_WEEKLY;
|
|
114
|
+
}
|
|
115
|
+
function encodeRewardFormula(f) {
|
|
116
|
+
return f === 'linear-v1' ? REWARD_FORMULA_LINEAR_V1 : REWARD_FORMULA_SAMMY_V1;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Vault-authority PDA. Owns all program-managed token accounts (reward
|
|
120
|
+
* vaults + pending vaults). Same value across the whole program.
|
|
121
|
+
*/
|
|
122
|
+
function deriveVaultAuthority(programId) {
|
|
123
|
+
return PublicKey.findProgramAddressSync([Buffer.from('vault-authority')], programId)[0];
|
|
124
|
+
}
|
|
125
|
+
function deriveRewardVault(vaultId, programId) {
|
|
126
|
+
return PublicKey.findProgramAddressSync([Buffer.from('reward-vault'), Buffer.from(vaultId)], programId)[0];
|
|
127
|
+
}
|
|
128
|
+
function deriveRewardVaultForPool(poolPda, programId) {
|
|
129
|
+
// staker-pool reward vault uses the pool PDA bytes as the vault id
|
|
130
|
+
return PublicKey.findProgramAddressSync([Buffer.from('reward-vault'), poolPda.toBytes()], programId)[0];
|
|
131
|
+
}
|
|
132
|
+
function derivePendingActionVault(configId, mint, actionType, programId) {
|
|
133
|
+
return PublicKey.findProgramAddressSync([
|
|
134
|
+
Buffer.from('pending-vault'),
|
|
135
|
+
Buffer.from(configId),
|
|
136
|
+
mint.toBytes(),
|
|
137
|
+
Buffer.from([actionType]),
|
|
138
|
+
], programId)[0];
|
|
139
|
+
}
|
|
140
|
+
export class EmblemStakeError extends Error {
|
|
141
|
+
code;
|
|
142
|
+
constructor(code, message) {
|
|
143
|
+
super(`[EmblemStake/${EmblemError[code]}] ${message}`);
|
|
144
|
+
this.name = 'EmblemStakeError';
|
|
145
|
+
this.code = code;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export class EmblemStakeClient {
|
|
149
|
+
connection;
|
|
150
|
+
programId;
|
|
151
|
+
signer;
|
|
152
|
+
commitment;
|
|
153
|
+
constructor(opts) {
|
|
154
|
+
this.connection = opts.connection;
|
|
155
|
+
this.programId = new PublicKey(opts.programId ?? STAKE_PROGRAM_ID);
|
|
156
|
+
this.signer = opts.signer;
|
|
157
|
+
this.commitment = opts.commitment ?? 'confirmed';
|
|
158
|
+
}
|
|
159
|
+
getProgramId() {
|
|
160
|
+
return this.programId.toBase58();
|
|
161
|
+
}
|
|
162
|
+
// ==========================================================================
|
|
163
|
+
// PDA derivation
|
|
164
|
+
// ==========================================================================
|
|
165
|
+
derivePoolPda(args) {
|
|
166
|
+
const { pda, bump } = deriveStakePoolPda({
|
|
167
|
+
asset: args.asset,
|
|
168
|
+
poolId: args.poolId,
|
|
169
|
+
programId: this.programId.toBase58(),
|
|
170
|
+
});
|
|
171
|
+
return { poolPda: pda.toBase58(), bump };
|
|
172
|
+
}
|
|
173
|
+
deriveVaultPda(args) {
|
|
174
|
+
const { pda, bump } = deriveStakeVaultPda({
|
|
175
|
+
poolPda: args.poolPda,
|
|
176
|
+
owner: args.owner,
|
|
177
|
+
programId: this.programId.toBase58(),
|
|
178
|
+
});
|
|
179
|
+
return { vaultPda: pda.toBase58(), bump };
|
|
180
|
+
}
|
|
181
|
+
deriveDistributionPda(args) {
|
|
182
|
+
const { pda, bump } = deriveRewardDistPda({
|
|
183
|
+
poolPda: args.poolPda,
|
|
184
|
+
periodId: args.periodId,
|
|
185
|
+
programId: this.programId.toBase58(),
|
|
186
|
+
});
|
|
187
|
+
return { distributionPda: pda.toBase58(), bump };
|
|
188
|
+
}
|
|
189
|
+
// ==========================================================================
|
|
190
|
+
// Reads
|
|
191
|
+
// ==========================================================================
|
|
192
|
+
async getPool(poolPda) {
|
|
193
|
+
const key = new PublicKey(poolPda);
|
|
194
|
+
const info = await this.connection.getAccountInfo(key, this.commitment);
|
|
195
|
+
if (!info)
|
|
196
|
+
return null;
|
|
197
|
+
if (!info.owner.equals(this.programId)) {
|
|
198
|
+
throw new EmblemStakeError(EmblemError.InvalidPda, `account ${poolPda} is owned by ${info.owner.toBase58()}, not the stake program ${this.programId.toBase58()}`);
|
|
199
|
+
}
|
|
200
|
+
const decoded = decodeStakePoolAccount(info.data);
|
|
201
|
+
return { ...decoded, poolPda };
|
|
202
|
+
}
|
|
203
|
+
async getVault(args) {
|
|
204
|
+
const owner = args.owner ?? this.signer?.publicKey;
|
|
205
|
+
if (!owner) {
|
|
206
|
+
throw new EmblemStakeError(EmblemError.MissingAccount, 'getVault requires either an explicit owner arg or a signer on the client');
|
|
207
|
+
}
|
|
208
|
+
const { vaultPda } = this.deriveVaultPda({ poolPda: args.poolPda, owner });
|
|
209
|
+
const key = new PublicKey(vaultPda);
|
|
210
|
+
const info = await this.connection.getAccountInfo(key, this.commitment);
|
|
211
|
+
if (!info)
|
|
212
|
+
return null;
|
|
213
|
+
if (!info.owner.equals(this.programId)) {
|
|
214
|
+
throw new EmblemStakeError(EmblemError.InvalidPda, `account ${vaultPda} is owned by ${info.owner.toBase58()}, not the stake program ${this.programId.toBase58()}`);
|
|
215
|
+
}
|
|
216
|
+
const decoded = decodeStakeVaultAccount(info.data);
|
|
217
|
+
return { ...decoded, vaultPda };
|
|
218
|
+
}
|
|
219
|
+
async getDistribution(distributionPda) {
|
|
220
|
+
const key = new PublicKey(distributionPda);
|
|
221
|
+
const info = await this.connection.getAccountInfo(key, this.commitment);
|
|
222
|
+
if (!info)
|
|
223
|
+
return null;
|
|
224
|
+
const decoded = decodeRewardDistributionAccount(info.data);
|
|
225
|
+
return { ...decoded, distributionPda };
|
|
226
|
+
}
|
|
227
|
+
/** List all vaults belonging to a pool. RPC-expensive on mainnet. */
|
|
228
|
+
async listVaultsByPool(poolPda) {
|
|
229
|
+
const poolKey = new PublicKey(poolPda);
|
|
230
|
+
// pool_pda field offset = disc(8) + bump(1) = 9.
|
|
231
|
+
const POOL_FIELD_OFFSET = 9;
|
|
232
|
+
const filters = [
|
|
233
|
+
{ memcmp: { offset: 0, bytes: bs58Encode(STAKE_VAULT_DISCRIMINATOR) } },
|
|
234
|
+
{ memcmp: { offset: POOL_FIELD_OFFSET, bytes: poolKey.toBase58() } },
|
|
235
|
+
];
|
|
236
|
+
const accounts = await this.connection.getProgramAccounts(this.programId, {
|
|
237
|
+
commitment: this.commitment,
|
|
238
|
+
filters,
|
|
239
|
+
});
|
|
240
|
+
return accounts.map((a) => {
|
|
241
|
+
const decoded = decodeStakeVaultAccount(a.account.data);
|
|
242
|
+
return { ...decoded, vaultPda: a.pubkey.toBase58() };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/** List all pools created by an authority. */
|
|
246
|
+
async listPoolsByAuthority(authority) {
|
|
247
|
+
const authorityKey = new PublicKey(authority);
|
|
248
|
+
const AUTHORITY_FIELD_OFFSET = 9;
|
|
249
|
+
const filters = [
|
|
250
|
+
{ memcmp: { offset: 0, bytes: bs58Encode(STAKE_POOL_DISCRIMINATOR) } },
|
|
251
|
+
{ memcmp: { offset: AUTHORITY_FIELD_OFFSET, bytes: authorityKey.toBase58() } },
|
|
252
|
+
];
|
|
253
|
+
const accounts = await this.connection.getProgramAccounts(this.programId, {
|
|
254
|
+
commitment: this.commitment,
|
|
255
|
+
filters,
|
|
256
|
+
});
|
|
257
|
+
return accounts.map((a) => {
|
|
258
|
+
const decoded = decodeStakePoolAccount(a.account.data);
|
|
259
|
+
return { ...decoded, poolPda: a.pubkey.toBase58() };
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// ==========================================================================
|
|
263
|
+
// Off-chain stake-mode evaluator — works today
|
|
264
|
+
// ==========================================================================
|
|
265
|
+
/**
|
|
266
|
+
* Evaluate "does this user pass a stake-mode gate?" given a vault account
|
|
267
|
+
* and a minimum threshold. Pure off-chain math; no chain calls.
|
|
268
|
+
*
|
|
269
|
+
* sdk/gate's evaluateOffChain stake-mode currently throws NotYetImplemented;
|
|
270
|
+
* with this client, you can compose the two yourself:
|
|
271
|
+
*
|
|
272
|
+
* const vault = await stake.getVault({ poolPda, owner: user });
|
|
273
|
+
* if (!vault) return { granted: false, evidence: { stakedAmount: 0, ... } };
|
|
274
|
+
* return stake.evaluateStakeGate(vault, minStake, nowUnixSeconds);
|
|
275
|
+
*
|
|
276
|
+
* "Granted" semantics: per 02-recipes.md §I and the GateRule shape, gate
|
|
277
|
+
* stake-mode is "user has at least N tokens currently staked in the named
|
|
278
|
+
* pool". We don't require the lock to still be active — the user is welcome
|
|
279
|
+
* to unstake (paying penalty) but for as long as the stake exists, the
|
|
280
|
+
* gate grants.
|
|
281
|
+
*/
|
|
282
|
+
evaluateStakeGate(vault, minStake, nowUnixSeconds) {
|
|
283
|
+
const stakedAmount = Number(vault.stakedAmount);
|
|
284
|
+
return {
|
|
285
|
+
granted: stakedAmount >= minStake,
|
|
286
|
+
evidence: {
|
|
287
|
+
stakedAmount,
|
|
288
|
+
lockEndsAt: vault.lockEndsAt,
|
|
289
|
+
lockExpired: nowUnixSeconds >= vault.lockEndsAt,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// ==========================================================================
|
|
294
|
+
// Instruction builders
|
|
295
|
+
// ==========================================================================
|
|
296
|
+
/**
|
|
297
|
+
* Build the create_pool instruction. Encodes the full StakePoolConfig
|
|
298
|
+
* including Vec<LockTier>, Vec<RewardSource>, and the tagged-enum
|
|
299
|
+
* PenaltyDestination.
|
|
300
|
+
*/
|
|
301
|
+
buildCreatePoolIx(args) {
|
|
302
|
+
const poolId = args.poolId ?? 0;
|
|
303
|
+
const cfg = args.config;
|
|
304
|
+
const asset = new PublicKey(cfg.asset);
|
|
305
|
+
const { poolPda } = this.derivePoolPda({
|
|
306
|
+
asset: cfg.asset,
|
|
307
|
+
poolId,
|
|
308
|
+
});
|
|
309
|
+
const lockTiers = cfg.lockOptions.map((t) => ({
|
|
310
|
+
days: t.days,
|
|
311
|
+
// Convert decimal multiplier (1.5) to fixed-point ×1000 (1500)
|
|
312
|
+
multiplier: Math.round(t.multiplier * MULTIPLIER_SCALE),
|
|
313
|
+
}));
|
|
314
|
+
const data = Buffer.concat([
|
|
315
|
+
IX_CREATE_POOL,
|
|
316
|
+
u32(poolId),
|
|
317
|
+
Buffer.from(asset.toBytes()),
|
|
318
|
+
bsOptString(cfg.hostUrl),
|
|
319
|
+
// Vec<LockTier>
|
|
320
|
+
u32(lockTiers.length),
|
|
321
|
+
...lockTiers.flatMap((t) => [u32(t.days), u32(t.multiplier)]),
|
|
322
|
+
u64(BigInt(cfg.minStake)),
|
|
323
|
+
bsOptU64(cfg.maxStakePerVault != null ? BigInt(cfg.maxStakePerVault) : null),
|
|
324
|
+
u16(cfg.earlyExitPenaltyBps),
|
|
325
|
+
encodePenaltyDestination(cfg.penaltyDestination),
|
|
326
|
+
// Vec<RewardSource>
|
|
327
|
+
u32(cfg.rewardSources.length),
|
|
328
|
+
...cfg.rewardSources.map(encodeRewardSource),
|
|
329
|
+
Buffer.from([encodeRewardCadence(cfg.rewardCadence)]),
|
|
330
|
+
Buffer.from([encodeRewardFormula(cfg.rewardFormula)]),
|
|
331
|
+
]);
|
|
332
|
+
return new TransactionInstruction({
|
|
333
|
+
programId: this.programId,
|
|
334
|
+
keys: [
|
|
335
|
+
{ pubkey: new PublicKey(args.authority), isSigner: true, isWritable: true },
|
|
336
|
+
{ pubkey: new PublicKey(poolPda), isSigner: false, isWritable: true },
|
|
337
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
338
|
+
],
|
|
339
|
+
data,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/** Build the register_reward_vault instruction (Tier 2 settlement bucket). */
|
|
343
|
+
buildRegisterRewardVaultIx(args) {
|
|
344
|
+
const mint = new PublicKey(args.mint);
|
|
345
|
+
const vaultAuthority = deriveVaultAuthority(this.programId);
|
|
346
|
+
const vault = deriveRewardVault(args.vaultId, this.programId);
|
|
347
|
+
return new TransactionInstruction({
|
|
348
|
+
programId: this.programId,
|
|
349
|
+
keys: [
|
|
350
|
+
{ pubkey: new PublicKey(args.authority), isSigner: true, isWritable: true },
|
|
351
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
352
|
+
{ pubkey: vaultAuthority, isSigner: false, isWritable: false },
|
|
353
|
+
{ pubkey: vault, isSigner: false, isWritable: true },
|
|
354
|
+
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
355
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
356
|
+
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
|
|
357
|
+
],
|
|
358
|
+
data: Buffer.concat([IX_REGISTER_REWARD_VAULT, bsString(args.vaultId)]),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
/** Build the register_pending_action_vault instruction (Tier 3 settlement bucket). */
|
|
362
|
+
buildRegisterPendingActionVaultIx(args) {
|
|
363
|
+
const mint = new PublicKey(args.mint);
|
|
364
|
+
const actionByte = args.actionType === 'buyback-burn' ? ACTION_TYPE_BUYBACK_BURN : ACTION_TYPE_LP_REINFORCE;
|
|
365
|
+
const vaultAuthority = deriveVaultAuthority(this.programId);
|
|
366
|
+
const vault = derivePendingActionVault(args.configId, mint, actionByte, this.programId);
|
|
367
|
+
return new TransactionInstruction({
|
|
368
|
+
programId: this.programId,
|
|
369
|
+
keys: [
|
|
370
|
+
{ pubkey: new PublicKey(args.authority), isSigner: true, isWritable: true },
|
|
371
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
372
|
+
{ pubkey: vaultAuthority, isSigner: false, isWritable: false },
|
|
373
|
+
{ pubkey: vault, isSigner: false, isWritable: true },
|
|
374
|
+
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
375
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
376
|
+
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
|
|
377
|
+
],
|
|
378
|
+
data: Buffer.concat([
|
|
379
|
+
IX_REGISTER_PENDING_ACTION_VAULT,
|
|
380
|
+
bsString(args.configId),
|
|
381
|
+
Buffer.from([actionByte]),
|
|
382
|
+
]),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/** Address of the staker-pool reward vault (Tier 2 destination for StakerPool recipients). */
|
|
386
|
+
deriveStakerPoolRewardVault(poolPda) {
|
|
387
|
+
return deriveRewardVaultForPool(new PublicKey(poolPda), this.programId).toBase58();
|
|
388
|
+
}
|
|
389
|
+
/** Address of a named distribution-vault (Tier 2 destination for DistributionVault recipients). */
|
|
390
|
+
deriveDistributionVaultAddress(vaultId) {
|
|
391
|
+
return deriveRewardVault(vaultId, this.programId).toBase58();
|
|
392
|
+
}
|
|
393
|
+
/** Address of a Tier 3 pending-action vault. */
|
|
394
|
+
derivePendingActionVaultAddress(args) {
|
|
395
|
+
const actionByte = args.actionType === 'buyback-burn' ? ACTION_TYPE_BUYBACK_BURN : ACTION_TYPE_LP_REINFORCE;
|
|
396
|
+
return derivePendingActionVault(args.configId, new PublicKey(args.mint), actionByte, this.programId).toBase58();
|
|
397
|
+
}
|
|
398
|
+
/** Vault-authority PDA — owns all program-managed token accounts. */
|
|
399
|
+
deriveVaultAuthorityAddress() {
|
|
400
|
+
return deriveVaultAuthority(this.programId).toBase58();
|
|
401
|
+
}
|
|
402
|
+
/** Pool's principal vault — where staked principal accumulates. */
|
|
403
|
+
derivePrincipalVaultAddress(poolPda) {
|
|
404
|
+
return PublicKey.findProgramAddressSync([Buffer.from('principal-vault'), new PublicKey(poolPda).toBytes()], this.programId)[0].toBase58();
|
|
405
|
+
}
|
|
406
|
+
/** Per-staker StakeVault metadata PDA. */
|
|
407
|
+
deriveStakeVaultAddress(args) {
|
|
408
|
+
return PublicKey.findProgramAddressSync([
|
|
409
|
+
Buffer.from('stake-vault'),
|
|
410
|
+
new PublicKey(args.poolPda).toBytes(),
|
|
411
|
+
new PublicKey(args.owner).toBytes(),
|
|
412
|
+
], this.programId)[0].toBase58();
|
|
413
|
+
}
|
|
414
|
+
/** RewardDistribution PDA for a (pool, period_id) pair. */
|
|
415
|
+
deriveRewardDistributionAddress(args) {
|
|
416
|
+
const periodBytes = Buffer.alloc(4);
|
|
417
|
+
periodBytes.writeUInt32LE(args.periodId, 0);
|
|
418
|
+
return PublicKey.findProgramAddressSync([Buffer.from('reward-dist'), new PublicKey(args.poolPda).toBytes(), periodBytes], this.programId)[0].toBase58();
|
|
419
|
+
}
|
|
420
|
+
// ==========================================================================
|
|
421
|
+
// Stake-flow ix builders
|
|
422
|
+
// ==========================================================================
|
|
423
|
+
/** Build the register_pool_token_accounts ix. Permissionless; one-time per pool. */
|
|
424
|
+
buildRegisterPoolTokenAccountsIx(args) {
|
|
425
|
+
const poolKey = new PublicKey(args.poolPda);
|
|
426
|
+
const principalVault = new PublicKey(this.derivePrincipalVaultAddress(args.poolPda));
|
|
427
|
+
const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
|
|
428
|
+
return new TransactionInstruction({
|
|
429
|
+
programId: this.programId,
|
|
430
|
+
keys: [
|
|
431
|
+
{ pubkey: new PublicKey(args.payer), isSigner: true, isWritable: true },
|
|
432
|
+
{ pubkey: poolKey, isSigner: false, isWritable: false },
|
|
433
|
+
{ pubkey: new PublicKey(args.mint), isSigner: false, isWritable: false },
|
|
434
|
+
{
|
|
435
|
+
pubkey: deriveVaultAuthority(this.programId),
|
|
436
|
+
isSigner: false,
|
|
437
|
+
isWritable: false,
|
|
438
|
+
},
|
|
439
|
+
{ pubkey: principalVault, isSigner: false, isWritable: true },
|
|
440
|
+
{ pubkey: rewardVault, isSigner: false, isWritable: true },
|
|
441
|
+
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
442
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
443
|
+
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
|
|
444
|
+
],
|
|
445
|
+
data: IX_REGISTER_POOL_TOKEN_ACCOUNTS,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/** Build the stake ix. */
|
|
449
|
+
buildStakeIx(args) {
|
|
450
|
+
const poolKey = new PublicKey(args.poolPda);
|
|
451
|
+
const stakeVault = new PublicKey(this.deriveStakeVaultAddress({ poolPda: args.poolPda, owner: args.user }));
|
|
452
|
+
const principalVault = new PublicKey(this.derivePrincipalVaultAddress(args.poolPda));
|
|
453
|
+
const amountBuf = Buffer.alloc(8);
|
|
454
|
+
amountBuf.writeBigUInt64LE(args.amount, 0);
|
|
455
|
+
const data = Buffer.concat([
|
|
456
|
+
IX_STAKE,
|
|
457
|
+
amountBuf,
|
|
458
|
+
Buffer.from([args.lockTierIdx]),
|
|
459
|
+
]);
|
|
460
|
+
return new TransactionInstruction({
|
|
461
|
+
programId: this.programId,
|
|
462
|
+
keys: [
|
|
463
|
+
{ pubkey: new PublicKey(args.user), isSigner: true, isWritable: true },
|
|
464
|
+
{ pubkey: poolKey, isSigner: false, isWritable: true },
|
|
465
|
+
{ pubkey: stakeVault, isSigner: false, isWritable: true },
|
|
466
|
+
{ pubkey: principalVault, isSigner: false, isWritable: true },
|
|
467
|
+
{
|
|
468
|
+
pubkey: new PublicKey(args.userTokenAccount),
|
|
469
|
+
isSigner: false,
|
|
470
|
+
isWritable: true,
|
|
471
|
+
},
|
|
472
|
+
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
473
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
474
|
+
],
|
|
475
|
+
data,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/** Build the unstake ix.
|
|
479
|
+
*
|
|
480
|
+
* Account requirements depend on the pool's penalty_destination:
|
|
481
|
+
* - `Redistribute`: only `mint` is required; pass any valid mint pubkey
|
|
482
|
+
* (constraint validates it equals pool.asset). penaltyDestinationAta
|
|
483
|
+
* can be omitted.
|
|
484
|
+
* - `Burn`: pass the pool's asset mint as `mint`. The program calls
|
|
485
|
+
* `token::burn` against it, decrementing supply atomically.
|
|
486
|
+
* penaltyDestinationAta is unused.
|
|
487
|
+
* - `TreasuryPda`: pass `mint` AND `penaltyDestinationAta` (the
|
|
488
|
+
* treasury PDA's ATA for the asset).
|
|
489
|
+
*
|
|
490
|
+
* `mint` must equal the pool's configured asset mint. The Anchor
|
|
491
|
+
* constraint enforces this on chain.
|
|
492
|
+
*/
|
|
493
|
+
buildUnstakeIx(args) {
|
|
494
|
+
const poolKey = new PublicKey(args.poolPda);
|
|
495
|
+
const stakeVault = new PublicKey(this.deriveStakeVaultAddress({ poolPda: args.poolPda, owner: args.user }));
|
|
496
|
+
const principalVault = new PublicKey(this.derivePrincipalVaultAddress(args.poolPda));
|
|
497
|
+
const keys = [
|
|
498
|
+
{ pubkey: new PublicKey(args.user), isSigner: true, isWritable: true },
|
|
499
|
+
{ pubkey: poolKey, isSigner: false, isWritable: true },
|
|
500
|
+
{ pubkey: stakeVault, isSigner: false, isWritable: true },
|
|
501
|
+
{
|
|
502
|
+
pubkey: deriveVaultAuthority(this.programId),
|
|
503
|
+
isSigner: false,
|
|
504
|
+
isWritable: false,
|
|
505
|
+
},
|
|
506
|
+
// mint — slot order matches programs/stake/src/lib.rs::UnstakeAccounts.
|
|
507
|
+
// Writable because token::burn (when penalty_destination is Burn)
|
|
508
|
+
// decrements the mint's supply field.
|
|
509
|
+
{ pubkey: new PublicKey(args.mint), isSigner: false, isWritable: true },
|
|
510
|
+
{ pubkey: principalVault, isSigner: false, isWritable: true },
|
|
511
|
+
{
|
|
512
|
+
pubkey: new PublicKey(args.userTokenAccount),
|
|
513
|
+
isSigner: false,
|
|
514
|
+
isWritable: true,
|
|
515
|
+
},
|
|
516
|
+
];
|
|
517
|
+
// penalty_destination_ata is `Option<Account<TokenAccount>>` in Rust.
|
|
518
|
+
// When provided (TreasuryPda destination), pass the real ATA mut.
|
|
519
|
+
// When omitted (Burn / Redistribute), pass the program id as the
|
|
520
|
+
// None sentinel — Anchor recognizes this as Option::None and skips
|
|
521
|
+
// deserialization + writability checks for the slot.
|
|
522
|
+
if (args.penaltyDestinationAta) {
|
|
523
|
+
keys.push({
|
|
524
|
+
pubkey: new PublicKey(args.penaltyDestinationAta),
|
|
525
|
+
isSigner: false,
|
|
526
|
+
isWritable: true,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
keys.push({ pubkey: this.programId, isSigner: false, isWritable: false });
|
|
531
|
+
}
|
|
532
|
+
keys.push({ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false });
|
|
533
|
+
return new TransactionInstruction({
|
|
534
|
+
programId: this.programId,
|
|
535
|
+
keys,
|
|
536
|
+
data: IX_UNSTAKE,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Build the claim ix. The caller must pass RewardDistribution PDAs
|
|
541
|
+
* for every period from `last_claim_period` to `current_period - 1`
|
|
542
|
+
* inclusive in `remaining_accounts`.
|
|
543
|
+
*/
|
|
544
|
+
buildClaimIx(args) {
|
|
545
|
+
const poolKey = new PublicKey(args.poolPda);
|
|
546
|
+
const stakeVault = new PublicKey(this.deriveStakeVaultAddress({ poolPda: args.poolPda, owner: args.user }));
|
|
547
|
+
const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
|
|
548
|
+
const distAccounts = args.periodIds.map((pid) => ({
|
|
549
|
+
pubkey: new PublicKey(this.deriveRewardDistributionAddress({ poolPda: args.poolPda, periodId: pid })),
|
|
550
|
+
isSigner: false,
|
|
551
|
+
isWritable: false,
|
|
552
|
+
}));
|
|
553
|
+
return new TransactionInstruction({
|
|
554
|
+
programId: this.programId,
|
|
555
|
+
keys: [
|
|
556
|
+
{ pubkey: new PublicKey(args.user), isSigner: true, isWritable: false },
|
|
557
|
+
{ pubkey: poolKey, isSigner: false, isWritable: false },
|
|
558
|
+
{ pubkey: stakeVault, isSigner: false, isWritable: true },
|
|
559
|
+
{
|
|
560
|
+
pubkey: deriveVaultAuthority(this.programId),
|
|
561
|
+
isSigner: false,
|
|
562
|
+
isWritable: false,
|
|
563
|
+
},
|
|
564
|
+
{ pubkey: rewardVault, isSigner: false, isWritable: true },
|
|
565
|
+
{
|
|
566
|
+
pubkey: new PublicKey(args.userTokenAccount),
|
|
567
|
+
isSigner: false,
|
|
568
|
+
isWritable: true,
|
|
569
|
+
},
|
|
570
|
+
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
571
|
+
...distAccounts,
|
|
572
|
+
],
|
|
573
|
+
data: IX_CLAIM,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Build the distribute_rewards ix. Permissionless trigger — anyone
|
|
578
|
+
* pays tx fees to advance the period.
|
|
579
|
+
*/
|
|
580
|
+
buildDistributeRewardsIx(args) {
|
|
581
|
+
const poolKey = new PublicKey(args.poolPda);
|
|
582
|
+
const rewardVault = new PublicKey(this.deriveStakerPoolRewardVault(args.poolPda));
|
|
583
|
+
const distribution = new PublicKey(this.deriveRewardDistributionAddress({
|
|
584
|
+
poolPda: args.poolPda,
|
|
585
|
+
periodId: args.periodId,
|
|
586
|
+
}));
|
|
587
|
+
return new TransactionInstruction({
|
|
588
|
+
programId: this.programId,
|
|
589
|
+
keys: [
|
|
590
|
+
{ pubkey: new PublicKey(args.payer), isSigner: true, isWritable: true },
|
|
591
|
+
{ pubkey: poolKey, isSigner: false, isWritable: true },
|
|
592
|
+
{ pubkey: rewardVault, isSigner: false, isWritable: false },
|
|
593
|
+
{ pubkey: distribution, isSigner: false, isWritable: true },
|
|
594
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
595
|
+
],
|
|
596
|
+
data: IX_DISTRIBUTE_REWARDS,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
// ==========================================================================
|
|
600
|
+
// Self-submitting wrappers
|
|
601
|
+
// ==========================================================================
|
|
602
|
+
async createPool(args) {
|
|
603
|
+
const signer = this.requireSigner('createPool');
|
|
604
|
+
const ix = this.buildCreatePoolIx({ ...args, authority: signer.publicKey });
|
|
605
|
+
const tx = new Transaction().add(ix);
|
|
606
|
+
const txSignature = await this.signAndSubmit(tx, signer);
|
|
607
|
+
const { poolPda } = this.derivePoolPda({
|
|
608
|
+
asset: args.config.asset,
|
|
609
|
+
poolId: args.poolId ?? 0,
|
|
610
|
+
});
|
|
611
|
+
return { poolPda, txSignature };
|
|
612
|
+
}
|
|
613
|
+
async registerRewardVault(args) {
|
|
614
|
+
const signer = this.requireSigner('registerRewardVault');
|
|
615
|
+
const ix = this.buildRegisterRewardVaultIx({ ...args, authority: signer.publicKey });
|
|
616
|
+
const tx = new Transaction().add(ix);
|
|
617
|
+
const txSignature = await this.signAndSubmit(tx, signer);
|
|
618
|
+
return { vaultPda: this.deriveDistributionVaultAddress(args.vaultId), txSignature };
|
|
619
|
+
}
|
|
620
|
+
async registerPendingActionVault(args) {
|
|
621
|
+
const signer = this.requireSigner('registerPendingActionVault');
|
|
622
|
+
const ix = this.buildRegisterPendingActionVaultIx({
|
|
623
|
+
...args,
|
|
624
|
+
authority: signer.publicKey,
|
|
625
|
+
});
|
|
626
|
+
const tx = new Transaction().add(ix);
|
|
627
|
+
const txSignature = await this.signAndSubmit(tx, signer);
|
|
628
|
+
return {
|
|
629
|
+
vaultPda: this.derivePendingActionVaultAddress(args),
|
|
630
|
+
txSignature,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Stake `amount` of the pool's asset for `lockTierIdx` from the
|
|
635
|
+
* configured signer. Caller must pass `userTokenAccount` (their ATA
|
|
636
|
+
* for the pool's mint) — the SDK doesn't auto-derive it because the
|
|
637
|
+
* caller may want to pre-create the ATA in the same tx.
|
|
638
|
+
*/
|
|
639
|
+
async stake(args) {
|
|
640
|
+
const signer = this.requireSigner('stake');
|
|
641
|
+
const ix = this.buildStakeIx({
|
|
642
|
+
user: signer.publicKey,
|
|
643
|
+
poolPda: args.poolPda,
|
|
644
|
+
userTokenAccount: args.userTokenAccount,
|
|
645
|
+
amount: args.amount,
|
|
646
|
+
lockTierIdx: args.lockTierIdx,
|
|
647
|
+
});
|
|
648
|
+
const tx = new Transaction().add(ix);
|
|
649
|
+
const txSignature = await this.signAndSubmit(tx, signer);
|
|
650
|
+
const vaultPda = this.deriveStakeVaultAddress({
|
|
651
|
+
poolPda: args.poolPda,
|
|
652
|
+
owner: signer.publicKey,
|
|
653
|
+
});
|
|
654
|
+
return { vaultPda, txSignature };
|
|
655
|
+
}
|
|
656
|
+
async unstake(args) {
|
|
657
|
+
const signer = this.requireSigner('unstake');
|
|
658
|
+
const ix = this.buildUnstakeIx({
|
|
659
|
+
user: signer.publicKey,
|
|
660
|
+
poolPda: args.poolPda,
|
|
661
|
+
mint: args.mint,
|
|
662
|
+
userTokenAccount: args.userTokenAccount,
|
|
663
|
+
penaltyDestinationAta: args.penaltyDestinationAta,
|
|
664
|
+
});
|
|
665
|
+
const tx = new Transaction().add(ix);
|
|
666
|
+
const txSignature = await this.signAndSubmit(tx, signer);
|
|
667
|
+
return { txSignature };
|
|
668
|
+
}
|
|
669
|
+
async claimRewards(args) {
|
|
670
|
+
const signer = this.requireSigner('claimRewards');
|
|
671
|
+
const ix = this.buildClaimIx({
|
|
672
|
+
user: signer.publicKey,
|
|
673
|
+
poolPda: args.poolPda,
|
|
674
|
+
userTokenAccount: args.userTokenAccount,
|
|
675
|
+
periodIds: args.periodIds,
|
|
676
|
+
});
|
|
677
|
+
const tx = new Transaction().add(ix);
|
|
678
|
+
const txSignature = await this.signAndSubmit(tx, signer);
|
|
679
|
+
return { txSignature };
|
|
680
|
+
}
|
|
681
|
+
async distributeRewards(args) {
|
|
682
|
+
const signer = this.requireSigner('distributeRewards');
|
|
683
|
+
const ix = this.buildDistributeRewardsIx({
|
|
684
|
+
payer: signer.publicKey,
|
|
685
|
+
poolPda: args.poolPda,
|
|
686
|
+
periodId: args.periodId,
|
|
687
|
+
});
|
|
688
|
+
const tx = new Transaction().add(ix);
|
|
689
|
+
const txSignature = await this.signAndSubmit(tx, signer);
|
|
690
|
+
return {
|
|
691
|
+
distributionPda: this.deriveRewardDistributionAddress({
|
|
692
|
+
poolPda: args.poolPda,
|
|
693
|
+
periodId: args.periodId,
|
|
694
|
+
}),
|
|
695
|
+
txSignature,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
async signAndSubmit(tx, signer) {
|
|
699
|
+
const { blockhash } = await this.connection.getLatestBlockhash(this.commitment);
|
|
700
|
+
tx.recentBlockhash = blockhash;
|
|
701
|
+
tx.feePayer = new PublicKey(signer.publicKey);
|
|
702
|
+
const signed = (await signer.signTransaction(tx));
|
|
703
|
+
const sig = await this.connection.sendRawTransaction(signed.serialize());
|
|
704
|
+
await this.connection.confirmTransaction(sig, this.commitment);
|
|
705
|
+
return sig;
|
|
706
|
+
}
|
|
707
|
+
requireSigner(op) {
|
|
708
|
+
if (!this.signer) {
|
|
709
|
+
throw new EmblemStakeError(EmblemError.Unauthorized, `${op}() requires a signer; construct EmblemStakeClient with { signer }`);
|
|
710
|
+
}
|
|
711
|
+
return this.signer;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
export function createStakeReader(opts) {
|
|
715
|
+
return new EmblemStakeClient({
|
|
716
|
+
connection: opts.connection,
|
|
717
|
+
programId: opts.programId ?? STAKE_PROGRAM_ID,
|
|
718
|
+
commitment: opts.commitment,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
// ============================================================================
|
|
722
|
+
// helpers
|
|
723
|
+
// ============================================================================
|
|
724
|
+
function bs58Encode(bytes) {
|
|
725
|
+
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
726
|
+
let zeros = 0;
|
|
727
|
+
while (zeros < bytes.length && bytes[zeros] === 0)
|
|
728
|
+
zeros++;
|
|
729
|
+
const digits = [];
|
|
730
|
+
for (let i = zeros; i < bytes.length; i++) {
|
|
731
|
+
let carry = bytes[i];
|
|
732
|
+
for (let j = 0; j < digits.length; j++) {
|
|
733
|
+
carry += digits[j] * 256;
|
|
734
|
+
digits[j] = carry % 58;
|
|
735
|
+
carry = (carry / 58) | 0;
|
|
736
|
+
}
|
|
737
|
+
while (carry > 0) {
|
|
738
|
+
digits.push(carry % 58);
|
|
739
|
+
carry = (carry / 58) | 0;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
let result = '';
|
|
743
|
+
for (let i = 0; i < zeros; i++)
|
|
744
|
+
result += ALPHABET[0];
|
|
745
|
+
for (let i = digits.length - 1; i >= 0; i--)
|
|
746
|
+
result += ALPHABET[digits[i]];
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
//# sourceMappingURL=client.js.map
|