@ardrive/turbo-sdk 1.42.0-alpha.2 → 1.42.0-alpha.4
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/lib/cjs/cli/cli.js +51 -0
- package/lib/cjs/cli/commands/arns.js +249 -0
- package/lib/cjs/cli/commands/freeStatus.js +47 -0
- package/lib/cjs/cli/commands/index.js +2 -0
- package/lib/cjs/cli/options.js +97 -1
- package/lib/cjs/common/http.js +4 -3
- package/lib/cjs/common/index.js +3 -0
- package/lib/cjs/common/payment.js +212 -0
- package/lib/cjs/common/signer.js +11 -3
- package/lib/cjs/common/turbo.js +54 -0
- package/lib/cjs/types.js +8 -1
- package/lib/cjs/utils/errors.js +19 -1
- package/lib/cjs/utils/uuid.js +31 -0
- package/lib/cjs/web/signer.js +4 -2
- package/lib/esm/cli/cli.js +53 -2
- package/lib/esm/cli/commands/arns.js +233 -0
- package/lib/esm/cli/commands/freeStatus.js +44 -0
- package/lib/esm/cli/commands/index.js +2 -0
- package/lib/esm/cli/options.js +96 -0
- package/lib/esm/common/http.js +4 -3
- package/lib/esm/common/index.js +3 -0
- package/lib/esm/common/payment.js +212 -0
- package/lib/esm/common/signer.js +11 -3
- package/lib/esm/common/turbo.js +54 -0
- package/lib/esm/types.js +7 -0
- package/lib/esm/utils/errors.js +17 -0
- package/lib/esm/utils/uuid.js +28 -0
- package/lib/esm/web/signer.js +4 -2
- package/lib/types/cli/commands/arns.d.ts +88 -0
- package/lib/types/cli/commands/arns.d.ts.map +1 -0
- package/lib/types/cli/commands/freeStatus.d.ts +3 -0
- package/lib/types/cli/commands/freeStatus.d.ts.map +1 -0
- package/lib/types/cli/commands/index.d.ts +2 -0
- package/lib/types/cli/commands/index.d.ts.map +1 -1
- package/lib/types/cli/options.d.ts +201 -0
- package/lib/types/cli/options.d.ts.map +1 -1
- package/lib/types/cli/types.d.ts +27 -0
- package/lib/types/cli/types.d.ts.map +1 -1
- package/lib/types/common/http.d.ts +2 -1
- package/lib/types/common/http.d.ts.map +1 -1
- package/lib/types/common/index.d.ts +1 -0
- package/lib/types/common/index.d.ts.map +1 -1
- package/lib/types/common/payment.d.ts +66 -1
- package/lib/types/common/payment.d.ts.map +1 -1
- package/lib/types/common/signer.d.ts +1 -1
- package/lib/types/common/signer.d.ts.map +1 -1
- package/lib/types/common/turbo.d.ts +59 -1
- package/lib/types/common/turbo.d.ts.map +1 -1
- package/lib/types/types.d.ts +148 -2
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/utils/errors.d.ts +14 -0
- package/lib/types/utils/errors.d.ts.map +1 -1
- package/lib/types/utils/uuid.d.ts +7 -0
- package/lib/types/utils/uuid.d.ts.map +1 -0
- package/lib/types/web/signer.d.ts +1 -1
- package/lib/types/web/signer.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { BigNumber } from 'bignumber.js';
|
|
17
|
+
import { TurboFactory } from '../../node/factory.js';
|
|
18
|
+
import { InsufficientCreditsError } from '../../utils/errors.js';
|
|
19
|
+
import { wincPerCredit } from '../constants.js';
|
|
20
|
+
import { configFromOptions, turboFromOptions } from '../utils.js';
|
|
21
|
+
export function requiredNameFromOptions(options) {
|
|
22
|
+
if (options.name === undefined || options.name.length === 0) {
|
|
23
|
+
throw new Error('Must provide an ArNS --name');
|
|
24
|
+
}
|
|
25
|
+
return options.name;
|
|
26
|
+
}
|
|
27
|
+
export function positiveIntFromOption(value, flag) {
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
throw new Error(`Must provide ${flag}`);
|
|
30
|
+
}
|
|
31
|
+
const num = +value;
|
|
32
|
+
if (!Number.isFinite(num) || !Number.isInteger(num) || num <= 0) {
|
|
33
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
34
|
+
}
|
|
35
|
+
return num;
|
|
36
|
+
}
|
|
37
|
+
export function typeFromOptions(value) {
|
|
38
|
+
if (value !== 'lease' && value !== 'permabuy') {
|
|
39
|
+
throw new Error("Must provide --type of 'lease' or 'permabuy'");
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
export function paidByFromArNSOptions(paidBy) {
|
|
44
|
+
return paidBy !== undefined && paidBy.length > 0 ? paidBy : undefined;
|
|
45
|
+
}
|
|
46
|
+
// A price quote depends only on the name (length), type, and years — NOT on the
|
|
47
|
+
// ANT the name will resolve to. The service ignores `processId` for pricing, but
|
|
48
|
+
// the SDK's Buy-Name validation still requires a non-empty one, so we substitute
|
|
49
|
+
// this obvious placeholder when the user omits `--process-id` from `arns-price`.
|
|
50
|
+
// (`buy-arns-name` still requires a real `--process-id`.)
|
|
51
|
+
const PRICING_PLACEHOLDER_PROCESS_ID = 'pricing-only-no-process-id';
|
|
52
|
+
/**
|
|
53
|
+
* Infer the ArNS pricing intent from the provided flags:
|
|
54
|
+
* - `--type` present -> Buy-Name (lease needs --years; --process-id optional for pricing)
|
|
55
|
+
* - `--increase-qty` present -> Increase-Undername-Limit
|
|
56
|
+
* - `--years` present (no type) -> Extend-Lease
|
|
57
|
+
* - otherwise -> Upgrade-Name
|
|
58
|
+
*/
|
|
59
|
+
export function arnsPriceParamsFromOptions(options) {
|
|
60
|
+
const name = requiredNameFromOptions(options);
|
|
61
|
+
if (options.type !== undefined) {
|
|
62
|
+
const type = typeFromOptions(options.type);
|
|
63
|
+
const processId = options.processId ?? PRICING_PLACEHOLDER_PROCESS_ID;
|
|
64
|
+
if (type === 'lease') {
|
|
65
|
+
return {
|
|
66
|
+
intent: 'Buy-Name',
|
|
67
|
+
name,
|
|
68
|
+
type: 'lease',
|
|
69
|
+
years: positiveIntFromOption(options.years, '--years'),
|
|
70
|
+
processId,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
intent: 'Buy-Name',
|
|
75
|
+
name,
|
|
76
|
+
type: 'permabuy',
|
|
77
|
+
processId,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (options.increaseQty !== undefined) {
|
|
81
|
+
return {
|
|
82
|
+
intent: 'Increase-Undername-Limit',
|
|
83
|
+
name,
|
|
84
|
+
increaseQty: positiveIntFromOption(options.increaseQty, '--increase-qty'),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (options.years !== undefined) {
|
|
88
|
+
return {
|
|
89
|
+
intent: 'Extend-Lease',
|
|
90
|
+
name,
|
|
91
|
+
years: positiveIntFromOption(options.years, '--years'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { intent: 'Upgrade-Name', name };
|
|
95
|
+
}
|
|
96
|
+
function creditsFromWinc(winc) {
|
|
97
|
+
return new BigNumber(winc).dividedBy(wincPerCredit).toFixed(12);
|
|
98
|
+
}
|
|
99
|
+
/** Rethrow a 402 as a clear, actionable "top up" message. */
|
|
100
|
+
async function withCreditErrorMapping(fn) {
|
|
101
|
+
try {
|
|
102
|
+
return await fn();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof InsufficientCreditsError) {
|
|
106
|
+
throw new Error('Insufficient Turbo credits to complete this ArNS purchase. ' +
|
|
107
|
+
'Top up your balance (e.g. `turbo top-up` or `turbo crypto-fund`) and retry.');
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function logPurchaseResult(action, result) {
|
|
113
|
+
console.log(JSON.stringify({
|
|
114
|
+
message: `${action} submitted!`,
|
|
115
|
+
nonce: result.nonce,
|
|
116
|
+
arioWriteId: result.arioWriteResult?.id,
|
|
117
|
+
purchaseReceipt: result.purchaseReceipt,
|
|
118
|
+
}, null, 2));
|
|
119
|
+
console.log(`\nTrack this purchase with:\n turbo arns-purchase-status --nonce ${result.nonce}`);
|
|
120
|
+
}
|
|
121
|
+
export async function arnsPrice(options, turbo) {
|
|
122
|
+
const params = arnsPriceParamsFromOptions(options);
|
|
123
|
+
const client = turbo ?? TurboFactory.unauthenticated(configFromOptions(options));
|
|
124
|
+
const { winc, mARIO } = await client.getArNSPriceForName(params);
|
|
125
|
+
console.log(JSON.stringify({
|
|
126
|
+
name: params.name,
|
|
127
|
+
intent: params.intent,
|
|
128
|
+
winc,
|
|
129
|
+
credits: creditsFromWinc(winc),
|
|
130
|
+
mARIO,
|
|
131
|
+
}, null, 2));
|
|
132
|
+
}
|
|
133
|
+
export async function buyArNSName(options, turbo) {
|
|
134
|
+
const name = requiredNameFromOptions(options);
|
|
135
|
+
const type = typeFromOptions(options.type);
|
|
136
|
+
const paidBy = paidByFromArNSOptions(options.paidBy);
|
|
137
|
+
// `--process-id` is OPTIONAL: omit it to have Turbo custodially provision the
|
|
138
|
+
// ANT (Turbo spawns + owns it — Model A); supply it to point the name at a
|
|
139
|
+
// user-owned ANT (Model B). When omitted, no `processId` is sent.
|
|
140
|
+
const processId = options.processId;
|
|
141
|
+
const client = turbo ?? (await turboFromOptions(options));
|
|
142
|
+
const result = await withCreditErrorMapping(() => type === 'lease'
|
|
143
|
+
? client.buyArNSName({
|
|
144
|
+
name,
|
|
145
|
+
type: 'lease',
|
|
146
|
+
years: positiveIntFromOption(options.years, '--years'),
|
|
147
|
+
processId,
|
|
148
|
+
paidBy,
|
|
149
|
+
})
|
|
150
|
+
: client.buyArNSName({ name, type: 'permabuy', processId, paidBy }));
|
|
151
|
+
logPurchaseResult('ArNS name purchase', result);
|
|
152
|
+
}
|
|
153
|
+
export async function extendArNSLease(options, turbo) {
|
|
154
|
+
const name = requiredNameFromOptions(options);
|
|
155
|
+
const years = positiveIntFromOption(options.years, '--years');
|
|
156
|
+
const paidBy = paidByFromArNSOptions(options.paidBy);
|
|
157
|
+
const client = turbo ?? (await turboFromOptions(options));
|
|
158
|
+
const result = await withCreditErrorMapping(() => client.extendArNSLease({ name, years, paidBy }));
|
|
159
|
+
logPurchaseResult('ArNS lease extension', result);
|
|
160
|
+
}
|
|
161
|
+
export async function increaseArNSUndernames(options, turbo) {
|
|
162
|
+
const name = requiredNameFromOptions(options);
|
|
163
|
+
const increaseQty = positiveIntFromOption(options.increaseQty, '--increase-qty');
|
|
164
|
+
const paidBy = paidByFromArNSOptions(options.paidBy);
|
|
165
|
+
const client = turbo ?? (await turboFromOptions(options));
|
|
166
|
+
const result = await withCreditErrorMapping(() => client.increaseArNSUndernameLimit({ name, increaseQty, paidBy }));
|
|
167
|
+
logPurchaseResult('ArNS undername limit increase', result);
|
|
168
|
+
}
|
|
169
|
+
export async function upgradeArNSName(options, turbo) {
|
|
170
|
+
const name = requiredNameFromOptions(options);
|
|
171
|
+
const paidBy = paidByFromArNSOptions(options.paidBy);
|
|
172
|
+
const client = turbo ?? (await turboFromOptions(options));
|
|
173
|
+
const result = await withCreditErrorMapping(() => client.upgradeArNSName({ name, paidBy }));
|
|
174
|
+
logPurchaseResult('ArNS name upgrade (to permabuy)', result);
|
|
175
|
+
}
|
|
176
|
+
export async function arnsPurchaseStatus(options, turbo) {
|
|
177
|
+
if (options.nonce === undefined || options.nonce.length === 0) {
|
|
178
|
+
throw new Error('Must provide a --nonce to look up purchase status');
|
|
179
|
+
}
|
|
180
|
+
const client = turbo ?? TurboFactory.unauthenticated(configFromOptions(options));
|
|
181
|
+
const status = await client.getArNSPurchaseStatus({ nonce: options.nonce });
|
|
182
|
+
const state = status.failedDate !== undefined
|
|
183
|
+
? 'failed'
|
|
184
|
+
: status.messageId
|
|
185
|
+
? 'success'
|
|
186
|
+
: 'pending';
|
|
187
|
+
console.log(JSON.stringify({ state, ...status }, null, 2));
|
|
188
|
+
}
|
|
189
|
+
export async function transferArNSAnt(options, turbo) {
|
|
190
|
+
if (options.antId === undefined) {
|
|
191
|
+
throw new Error('Must provide an --ant-id to transfer');
|
|
192
|
+
}
|
|
193
|
+
if (options.target === undefined) {
|
|
194
|
+
throw new Error('Must provide a --target address to transfer the ANT to');
|
|
195
|
+
}
|
|
196
|
+
const client = turbo ?? (await turboFromOptions(options));
|
|
197
|
+
const result = await client.transferArNSAnt({
|
|
198
|
+
antId: options.antId,
|
|
199
|
+
target: options.target,
|
|
200
|
+
});
|
|
201
|
+
console.log(JSON.stringify({ message: 'ANT transfer submitted!', ...result }, null, 2));
|
|
202
|
+
}
|
|
203
|
+
export async function setArNSRecord(options, turbo) {
|
|
204
|
+
if (options.antId === undefined) {
|
|
205
|
+
throw new Error('Must provide an --ant-id to set a record on');
|
|
206
|
+
}
|
|
207
|
+
if (options.transactionId === undefined) {
|
|
208
|
+
throw new Error('Must provide a --transaction-id for the record');
|
|
209
|
+
}
|
|
210
|
+
const ttlSeconds = positiveIntFromOption(options.ttlSeconds, '--ttl-seconds');
|
|
211
|
+
const client = turbo ?? (await turboFromOptions(options));
|
|
212
|
+
const result = await client.setArNSRecord({
|
|
213
|
+
antId: options.antId,
|
|
214
|
+
undername: options.undername ?? '@',
|
|
215
|
+
transactionId: options.transactionId,
|
|
216
|
+
ttlSeconds,
|
|
217
|
+
});
|
|
218
|
+
console.log(JSON.stringify({ message: 'ArNS record set!', ...result }, null, 2));
|
|
219
|
+
}
|
|
220
|
+
export async function removeArNSRecord(options, turbo) {
|
|
221
|
+
if (options.antId === undefined) {
|
|
222
|
+
throw new Error('Must provide an --ant-id to remove a record from');
|
|
223
|
+
}
|
|
224
|
+
if (options.undername === undefined || options.undername.length === 0) {
|
|
225
|
+
throw new Error('Must provide an --undername to remove');
|
|
226
|
+
}
|
|
227
|
+
const client = turbo ?? (await turboFromOptions(options));
|
|
228
|
+
const result = await client.removeArNSRecord({
|
|
229
|
+
antId: options.antId,
|
|
230
|
+
undername: options.undername,
|
|
231
|
+
});
|
|
232
|
+
console.log(JSON.stringify({ message: 'ArNS record removed!', ...result }, null, 2));
|
|
233
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { TurboFactory } from '../../node/factory.js';
|
|
17
|
+
import { addressOrPrivateKeyFromOptions, configFromOptions } from '../utils.js';
|
|
18
|
+
export async function freeStatus(options) {
|
|
19
|
+
const config = configFromOptions(options);
|
|
20
|
+
const { address, privateKey } = await addressOrPrivateKeyFromOptions(options);
|
|
21
|
+
const { bytesRemaining, nativeAddress } = await (async () => {
|
|
22
|
+
if (address !== undefined) {
|
|
23
|
+
return {
|
|
24
|
+
...(await TurboFactory.unauthenticated(config).getFreeStatus(address)),
|
|
25
|
+
nativeAddress: address,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (privateKey === undefined) {
|
|
29
|
+
throw new Error('Must provide an (--address) or use a valid wallet');
|
|
30
|
+
}
|
|
31
|
+
const turbo = TurboFactory.authenticated({
|
|
32
|
+
...config,
|
|
33
|
+
privateKey,
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
...(await turbo.getFreeStatus()),
|
|
37
|
+
nativeAddress: await turbo.signer.getNativeAddress(),
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
console.log(`Turbo free-tier allowance for Native Address "${nativeAddress}"\n` +
|
|
41
|
+
(bytesRemaining === null
|
|
42
|
+
? 'Free bytes remaining: unlimited (exempt/partner wallet)'
|
|
43
|
+
: `Free bytes remaining: ${bytesRemaining}`));
|
|
44
|
+
}
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
export * from './arns.js';
|
|
16
17
|
export * from './balance.js';
|
|
17
18
|
export * from './cryptoFund.js';
|
|
19
|
+
export * from './freeStatus.js';
|
|
18
20
|
export * from './price.js';
|
|
19
21
|
export * from './topUp.js';
|
|
20
22
|
export * from './uploadFile.js';
|
package/lib/esm/cli/options.js
CHANGED
|
@@ -186,6 +186,51 @@ export const optionMap = {
|
|
|
186
186
|
alias: '--max-crypto-top-up-value <maxCryptoTopUpValue>',
|
|
187
187
|
description: 'Maximum crypto top-up value to use for the upload. Defaults to no limit.',
|
|
188
188
|
},
|
|
189
|
+
// ---- ArNS (paid with Turbo Credits) ----
|
|
190
|
+
arnsName: {
|
|
191
|
+
alias: '--name <name>',
|
|
192
|
+
description: 'ArNS name to price, buy, or manage',
|
|
193
|
+
},
|
|
194
|
+
arnsType: {
|
|
195
|
+
alias: '--type <type>',
|
|
196
|
+
description: "ArNS purchase type for Buy-Name: 'lease' or 'permabuy'",
|
|
197
|
+
},
|
|
198
|
+
arnsYears: {
|
|
199
|
+
alias: '--years <years>',
|
|
200
|
+
description: 'Lease duration in years (Buy-Name lease / Extend-Lease)',
|
|
201
|
+
},
|
|
202
|
+
arnsIncreaseQty: {
|
|
203
|
+
alias: '--increase-qty <qty>',
|
|
204
|
+
description: 'Number of additional undernames (Increase-Undername-Limit)',
|
|
205
|
+
},
|
|
206
|
+
arnsProcessId: {
|
|
207
|
+
alias: '--process-id <processId>',
|
|
208
|
+
description: 'ANT process ID the ArNS name resolves to (Buy-Name). Optional: omit for Turbo custodial provisioning (Turbo owns the ANT); supply for a user-owned ANT',
|
|
209
|
+
},
|
|
210
|
+
arnsNonce: {
|
|
211
|
+
alias: '--nonce <nonce>',
|
|
212
|
+
description: 'ArNS purchase nonce to look up the status for',
|
|
213
|
+
},
|
|
214
|
+
arnsAntId: {
|
|
215
|
+
alias: '--ant-id <antId>',
|
|
216
|
+
description: 'ANT (Metaplex Core asset) ID to transfer or manage',
|
|
217
|
+
},
|
|
218
|
+
arnsTarget: {
|
|
219
|
+
alias: '--target <address>',
|
|
220
|
+
description: 'Target Solana pubkey to transfer the ANT to',
|
|
221
|
+
},
|
|
222
|
+
arnsUndername: {
|
|
223
|
+
alias: '--undername <undername>',
|
|
224
|
+
description: "ArNS undername record to set or remove (defaults to '@')",
|
|
225
|
+
},
|
|
226
|
+
arnsTransactionId: {
|
|
227
|
+
alias: '--transaction-id <transactionId>',
|
|
228
|
+
description: 'Arweave transaction ID the ArNS record resolves to',
|
|
229
|
+
},
|
|
230
|
+
arnsTtlSeconds: {
|
|
231
|
+
alias: '--ttl-seconds <ttlSeconds>',
|
|
232
|
+
description: 'TTL in seconds for the ArNS record',
|
|
233
|
+
},
|
|
189
234
|
};
|
|
190
235
|
export const walletOptions = [
|
|
191
236
|
optionMap.walletFile,
|
|
@@ -239,3 +284,54 @@ export const shareCreditsOptions = [
|
|
|
239
284
|
];
|
|
240
285
|
export const revokeCreditsOptions = [...walletOptions, optionMap.address];
|
|
241
286
|
export const listSharesOptions = revokeCreditsOptions;
|
|
287
|
+
// ---- ArNS command option sets (token + --payment-url come from globalOptions) ----
|
|
288
|
+
export const arnsPriceOptions = [
|
|
289
|
+
optionMap.arnsName,
|
|
290
|
+
optionMap.arnsType,
|
|
291
|
+
optionMap.arnsYears,
|
|
292
|
+
optionMap.arnsIncreaseQty,
|
|
293
|
+
optionMap.arnsProcessId,
|
|
294
|
+
];
|
|
295
|
+
export const buyArNSNameOptions = [
|
|
296
|
+
...walletOptions,
|
|
297
|
+
optionMap.arnsName,
|
|
298
|
+
optionMap.arnsType,
|
|
299
|
+
optionMap.arnsYears,
|
|
300
|
+
optionMap.arnsProcessId,
|
|
301
|
+
optionMap.paidBy,
|
|
302
|
+
];
|
|
303
|
+
export const extendArNSLeaseOptions = [
|
|
304
|
+
...walletOptions,
|
|
305
|
+
optionMap.arnsName,
|
|
306
|
+
optionMap.arnsYears,
|
|
307
|
+
optionMap.paidBy,
|
|
308
|
+
];
|
|
309
|
+
export const increaseArNSUndernamesOptions = [
|
|
310
|
+
...walletOptions,
|
|
311
|
+
optionMap.arnsName,
|
|
312
|
+
optionMap.arnsIncreaseQty,
|
|
313
|
+
optionMap.paidBy,
|
|
314
|
+
];
|
|
315
|
+
export const upgradeArNSNameOptions = [
|
|
316
|
+
...walletOptions,
|
|
317
|
+
optionMap.arnsName,
|
|
318
|
+
optionMap.paidBy,
|
|
319
|
+
];
|
|
320
|
+
export const arnsPurchaseStatusOptions = [optionMap.arnsNonce];
|
|
321
|
+
export const transferArNSAntOptions = [
|
|
322
|
+
...walletOptions,
|
|
323
|
+
optionMap.arnsAntId,
|
|
324
|
+
optionMap.arnsTarget,
|
|
325
|
+
];
|
|
326
|
+
export const setArNSRecordOptions = [
|
|
327
|
+
...walletOptions,
|
|
328
|
+
optionMap.arnsAntId,
|
|
329
|
+
optionMap.arnsUndername,
|
|
330
|
+
optionMap.arnsTransactionId,
|
|
331
|
+
optionMap.arnsTtlSeconds,
|
|
332
|
+
];
|
|
333
|
+
export const removeArNSRecordOptions = [
|
|
334
|
+
...walletOptions,
|
|
335
|
+
optionMap.arnsAntId,
|
|
336
|
+
optionMap.arnsUndername,
|
|
337
|
+
];
|
package/lib/esm/common/http.js
CHANGED
|
@@ -43,7 +43,7 @@ export class TurboHTTPService {
|
|
|
43
43
|
signal,
|
|
44
44
|
}), allowedStatuses);
|
|
45
45
|
}
|
|
46
|
-
async post({ endpoint, signal, allowedStatuses = [200, 202], headers, data, x402Options, }) {
|
|
46
|
+
async post({ endpoint, signal, allowedStatuses = [200, 202], headers, data, x402Options, retry = true, }) {
|
|
47
47
|
if (x402Options !== undefined) {
|
|
48
48
|
return this.x402Post({
|
|
49
49
|
signal,
|
|
@@ -55,9 +55,10 @@ export class TurboHTTPService {
|
|
|
55
55
|
}
|
|
56
56
|
// Convert all data types to fetch-compatible body
|
|
57
57
|
const { body, duplex } = await toFetchBody(data);
|
|
58
|
-
// Use retry for Buffer/Uint8Array, tryRequest for streams
|
|
58
|
+
// Use retry for Buffer/Uint8Array, tryRequest for streams. Callers can opt
|
|
59
|
+
// out of retry for non-idempotent signed writes via `retry: false`.
|
|
59
60
|
const isReusableData = data instanceof Buffer || data instanceof Uint8Array;
|
|
60
|
-
const requestFn = isReusableData
|
|
61
|
+
const requestFn = isReusableData && retry
|
|
61
62
|
? this.withRetry.bind(this)
|
|
62
63
|
: this.tryRequest.bind(this);
|
|
63
64
|
return requestFn(() => fetch(this.baseURL + endpoint, {
|
package/lib/esm/common/index.js
CHANGED
|
@@ -18,3 +18,6 @@ export * from './payment.js';
|
|
|
18
18
|
export * from './turbo.js';
|
|
19
19
|
export * from './currency.js';
|
|
20
20
|
export * from './token/index.js';
|
|
21
|
+
// Typed errors so consumers can `catch (e) { if (e instanceof InsufficientCreditsError) ... }`
|
|
22
|
+
// e.g. to prompt a top-up on a 402 ArNS purchase.
|
|
23
|
+
export * from '../utils/errors.js';
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { BigNumber } from 'bignumber.js';
|
|
17
|
+
import { arNSPurchaseIntents, } from '../types.js';
|
|
17
18
|
import { isAnyValidUserAddress } from '../utils/common.js';
|
|
19
|
+
import { FailedRequestError, InsufficientCreditsError, ProvidedInputError, } from '../utils/errors.js';
|
|
20
|
+
import { uuidV4 } from '../utils/uuid.js';
|
|
18
21
|
import { defaultRetryConfig } from './http.js';
|
|
19
22
|
import { TurboHTTPService } from './http.js';
|
|
20
23
|
import { Logger } from './logger.js';
|
|
@@ -47,6 +50,15 @@ export class TurboUnauthenticatedPaymentService {
|
|
|
47
50
|
receivedApprovals: [],
|
|
48
51
|
};
|
|
49
52
|
}
|
|
53
|
+
async getFreeStatus(address) {
|
|
54
|
+
const status = await this.httpService.get({
|
|
55
|
+
endpoint: `/account/free?address=${address}`,
|
|
56
|
+
allowedStatuses: [200, 404],
|
|
57
|
+
});
|
|
58
|
+
// Normalize: preserve a legitimate `0` (free tier off) or `null` (unlimited),
|
|
59
|
+
// and coerce a missing field (e.g. a 404 body) to `null`.
|
|
60
|
+
return { bytesRemaining: status?.bytesRemaining ?? null };
|
|
61
|
+
}
|
|
50
62
|
getFiatRates() {
|
|
51
63
|
return this.httpService.get({
|
|
52
64
|
endpoint: '/rates',
|
|
@@ -91,6 +103,90 @@ export class TurboUnauthenticatedPaymentService {
|
|
|
91
103
|
equivalentWincTokenAmount: actualPaymentAmount.toString(),
|
|
92
104
|
};
|
|
93
105
|
}
|
|
106
|
+
async getArNSPriceForName(params) {
|
|
107
|
+
// `async` so a validation failure surfaces as a rejected promise (consistent
|
|
108
|
+
// with `purchaseArNSName`) rather than a synchronous throw.
|
|
109
|
+
this.validateArNSPurchaseParams(params);
|
|
110
|
+
return this.httpService.get({
|
|
111
|
+
endpoint: `/arns/price/${params.intent.toLowerCase()}/${params.name}${this.buildArNSPurchaseQuery(params)}`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Fail fast (client-side) on malformed ArNS requests so JS callers that bypass
|
|
116
|
+
* the compile-time intent unions get a clear `ProvidedInputError` instead of an
|
|
117
|
+
* opaque service 4xx. Enforces the required fields per intent:
|
|
118
|
+
* - `Buy-Name`: `type` ('lease' | 'permabuy'); leases also need `years`.
|
|
119
|
+
* `processId` is OPTIONAL — omit it to have the bundler custodially
|
|
120
|
+
* provision the ANT (Turbo owns it), supply it for a user-owned ANT.
|
|
121
|
+
* - `Extend-Lease`: positive `years`
|
|
122
|
+
* - `Increase-Undername-Limit`: positive `increaseQty`
|
|
123
|
+
* - `Upgrade-Name`: just `name`
|
|
124
|
+
*/
|
|
125
|
+
validateArNSPurchaseParams(params) {
|
|
126
|
+
const p = params;
|
|
127
|
+
if (!arNSPurchaseIntents.includes(p.intent)) {
|
|
128
|
+
throw new ProvidedInputError(`Invalid ArNS intent '${p.intent}'. Expected one of: ${arNSPurchaseIntents.join(', ')}.`);
|
|
129
|
+
}
|
|
130
|
+
if (typeof p.name !== 'string' || p.name.length === 0) {
|
|
131
|
+
throw new ProvidedInputError('An ArNS `name` is required.');
|
|
132
|
+
}
|
|
133
|
+
const isPositiveNumber = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
|
|
134
|
+
switch (p.intent) {
|
|
135
|
+
case 'Buy-Name':
|
|
136
|
+
if (p.type !== 'lease' && p.type !== 'permabuy') {
|
|
137
|
+
throw new ProvidedInputError("Buy-Name requires a `type` of 'lease' or 'permabuy'.");
|
|
138
|
+
}
|
|
139
|
+
// `processId` is optional for Buy-Name: omitting it drives the
|
|
140
|
+
// bundler's custodial provisioning path (Turbo spawns + owns the ANT).
|
|
141
|
+
// If supplied it must be a non-empty string (user-owned ANT).
|
|
142
|
+
if (p.processId !== undefined &&
|
|
143
|
+
(typeof p.processId !== 'string' || p.processId.length === 0)) {
|
|
144
|
+
throw new ProvidedInputError('Buy-Name `processId`, when provided, must be a non-empty string (the ANT the name resolves to).');
|
|
145
|
+
}
|
|
146
|
+
if (p.type === 'lease' && !isPositiveNumber(p.years)) {
|
|
147
|
+
throw new ProvidedInputError('A lease `Buy-Name` requires a positive `years`.');
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
case 'Extend-Lease':
|
|
151
|
+
if (!isPositiveNumber(p.years)) {
|
|
152
|
+
throw new ProvidedInputError('Extend-Lease requires a positive `years`.');
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case 'Increase-Undername-Limit':
|
|
156
|
+
if (!isPositiveNumber(p.increaseQty)) {
|
|
157
|
+
throw new ProvidedInputError('Increase-Undername-Limit requires a positive `increaseQty`.');
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case 'Upgrade-Name':
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
getArNSPurchaseStatus({ nonce, }) {
|
|
165
|
+
return this.httpService.get({
|
|
166
|
+
endpoint: `/arns/purchase/${nonce}`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
buildArNSPurchaseQuery(input) {
|
|
170
|
+
// The intent-specific union members each carry only their own fields; read
|
|
171
|
+
// them through a single widened view rather than narrowing per intent.
|
|
172
|
+
const { type, years, increaseQty, processId, paidBy } = input;
|
|
173
|
+
const params = new URLSearchParams();
|
|
174
|
+
if (type !== undefined)
|
|
175
|
+
params.set('type', type);
|
|
176
|
+
if (years !== undefined)
|
|
177
|
+
params.set('years', `${years}`);
|
|
178
|
+
if (increaseQty !== undefined)
|
|
179
|
+
params.set('increaseQty', `${increaseQty}`);
|
|
180
|
+
if (processId !== undefined)
|
|
181
|
+
params.set('processId', processId);
|
|
182
|
+
if (paidBy !== undefined) {
|
|
183
|
+
for (const payer of Array.isArray(paidBy) ? paidBy : [paidBy]) {
|
|
184
|
+
params.append('paidBy', payer);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const query = params.toString();
|
|
188
|
+
return query.length > 0 ? `?${query}` : '';
|
|
189
|
+
}
|
|
94
190
|
appendPromoCodesToQuery(promoCodes) {
|
|
95
191
|
const promoCodesQuery = promoCodes.join(',');
|
|
96
192
|
return promoCodesQuery ? `promoCode=${promoCodesQuery}` : '';
|
|
@@ -255,6 +351,122 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
|
|
|
255
351
|
userAddress ??= await this.signer.getNativeAddress();
|
|
256
352
|
return super.getBalance(userAddress);
|
|
257
353
|
}
|
|
354
|
+
async getFreeStatus(userAddress) {
|
|
355
|
+
userAddress ??= await this.signer.getNativeAddress();
|
|
356
|
+
return super.getFreeStatus(userAddress);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Buy / extend / upgrade an ArNS name, paying with the signer's Turbo credit
|
|
360
|
+
* balance. The bundler performs the on-chain ARIO purchase and debits credits;
|
|
361
|
+
* a `402` (FailedRequestError.status === 402) indicates insufficient credits.
|
|
362
|
+
*/
|
|
363
|
+
async purchaseArNSName(params) {
|
|
364
|
+
this.validateArNSPurchaseParams(params);
|
|
365
|
+
// The bundler requires the signed nonce to be a UUID; it also doubles as
|
|
366
|
+
// the idempotency + status-lookup key (`getArNSPurchaseStatus`).
|
|
367
|
+
const nonce = uuidV4();
|
|
368
|
+
const headers = await this.signer.generateSignedRequestHeaders(nonce);
|
|
369
|
+
let response;
|
|
370
|
+
try {
|
|
371
|
+
response = await this.httpService.post({
|
|
372
|
+
endpoint: `/arns/purchase/${params.intent.toLowerCase()}/${params.name}${this.buildArNSPurchaseQuery(params)}`,
|
|
373
|
+
headers,
|
|
374
|
+
// Params travel in the query string + signed headers; the service reads
|
|
375
|
+
// no body, but the HTTP layer requires a `data` field.
|
|
376
|
+
data: Buffer.from([]),
|
|
377
|
+
// Non-idempotent signed write: the nonce is single-use, so a retried
|
|
378
|
+
// (but already-landed) purchase would 4xx as "already exists". Poll
|
|
379
|
+
// status by nonce instead of retrying.
|
|
380
|
+
retry: false,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
// Surface a credit shortfall as a typed, catchable error so callers can
|
|
385
|
+
// prompt a top-up. The `nonce` is the idempotency key: after topping up,
|
|
386
|
+
// retry the same purchase (a fresh nonce is fine — the service dedupes by
|
|
387
|
+
// the on-chain effect, and a captured nonce lets you poll status).
|
|
388
|
+
if (error instanceof FailedRequestError && error.status === 402) {
|
|
389
|
+
throw new InsufficientCreditsError(error.message);
|
|
390
|
+
}
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
// Normalize both nonce fields to the one we signed so callers can poll with
|
|
394
|
+
// either `response.nonce` or `response.purchaseReceipt.nonce`.
|
|
395
|
+
return {
|
|
396
|
+
...response,
|
|
397
|
+
nonce,
|
|
398
|
+
purchaseReceipt: { ...response.purchaseReceipt, nonce },
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
buyArNSName(params) {
|
|
402
|
+
return this.purchaseArNSName({
|
|
403
|
+
...params,
|
|
404
|
+
intent: 'Buy-Name',
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
extendArNSLease(params) {
|
|
408
|
+
return this.purchaseArNSName({ ...params, intent: 'Extend-Lease' });
|
|
409
|
+
}
|
|
410
|
+
increaseArNSUndernameLimit(params) {
|
|
411
|
+
return this.purchaseArNSName({
|
|
412
|
+
...params,
|
|
413
|
+
intent: 'Increase-Undername-Limit',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
upgradeArNSName(params) {
|
|
417
|
+
return this.purchaseArNSName({ ...params, intent: 'Upgrade-Name' });
|
|
418
|
+
}
|
|
419
|
+
// ---- ArNS ANT custody: self-custody exit + manage records ----
|
|
420
|
+
// Canonical ACTION-BOUND message. MUST match the bundler's
|
|
421
|
+
// buildArNSCustodyMessage byte-for-byte (newline-delimited) or every signature
|
|
422
|
+
// is rejected. The bundler reconstructs this from the request and verifies the
|
|
423
|
+
// signature over `message + nonce`, so a captured signature can't be replayed
|
|
424
|
+
// against a different operation/params.
|
|
425
|
+
buildArNSCustodyMessage(action, fields) {
|
|
426
|
+
return ['arns', action, ...fields].join('\n');
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Self-custody exit: move a Turbo-custodied ANT to a Solana pubkey you control.
|
|
430
|
+
* Authenticated with an action-bound, single-use signature.
|
|
431
|
+
*/
|
|
432
|
+
async transferArNSAnt({ antId, target, }) {
|
|
433
|
+
const nonce = uuidV4();
|
|
434
|
+
const headers = await this.signer.generateSignedRequestHeaders(nonce, this.buildArNSCustodyMessage('transfer', [antId, target]));
|
|
435
|
+
return this.httpService.post({
|
|
436
|
+
endpoint: `/arns/transfer/${antId}?target=${encodeURIComponent(target)}`,
|
|
437
|
+
headers,
|
|
438
|
+
data: Buffer.from([]),
|
|
439
|
+
retry: false, // single-use action-bound nonce; don't re-POST on 5xx
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
/** Set a resolution record on a custodied ANT (undername defaults to '@'). */
|
|
443
|
+
async setArNSRecord({ antId, undername = '@', transactionId, ttlSeconds, }) {
|
|
444
|
+
const nonce = uuidV4();
|
|
445
|
+
const headers = await this.signer.generateSignedRequestHeaders(nonce, this.buildArNSCustodyMessage('set-record', [
|
|
446
|
+
antId,
|
|
447
|
+
undername,
|
|
448
|
+
transactionId,
|
|
449
|
+
String(ttlSeconds),
|
|
450
|
+
]));
|
|
451
|
+
const query = `?undername=${encodeURIComponent(undername)}&transactionId=${transactionId}&ttlSeconds=${ttlSeconds}`;
|
|
452
|
+
return this.httpService.post({
|
|
453
|
+
endpoint: `/arns/manage/${antId}/set-record${query}`,
|
|
454
|
+
headers,
|
|
455
|
+
data: Buffer.from([]),
|
|
456
|
+
retry: false, // single-use action-bound nonce; don't re-POST on 5xx
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/** Remove a resolution record (an undername) from a custodied ANT. */
|
|
460
|
+
async removeArNSRecord({ antId, undername, }) {
|
|
461
|
+
const nonce = uuidV4();
|
|
462
|
+
const headers = await this.signer.generateSignedRequestHeaders(nonce, this.buildArNSCustodyMessage('remove-record', [antId, undername]));
|
|
463
|
+
return this.httpService.post({
|
|
464
|
+
endpoint: `/arns/manage/${antId}/remove-record?undername=${encodeURIComponent(undername)}`,
|
|
465
|
+
headers,
|
|
466
|
+
data: Buffer.from([]),
|
|
467
|
+
retry: false, // single-use action-bound nonce; don't re-POST on 5xx
|
|
468
|
+
});
|
|
469
|
+
}
|
|
258
470
|
async getCreditShareApprovals({ userAddress, }) {
|
|
259
471
|
userAddress ??= await this.signer.getNativeAddress();
|
|
260
472
|
return super.getCreditShareApprovals({ userAddress });
|