@ardrive/turbo-sdk 2.1.0-alpha.1 → 2.1.0-alpha.3
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/common/factory.js +1 -1
- package/lib/cjs/common/payment.js +58 -11
- package/lib/cjs/common/token/spl.js +32 -4
- package/lib/cjs/common/turbo.js +7 -3
- package/lib/cjs/types.js +11 -1
- package/lib/cjs/utils/errors.js +28 -1
- package/lib/cjs/version.js +1 -1
- package/lib/esm/common/factory.js +2 -2
- package/lib/esm/common/payment.js +59 -12
- package/lib/esm/common/token/spl.js +32 -4
- package/lib/esm/common/turbo.js +7 -3
- package/lib/esm/types.js +10 -0
- package/lib/esm/utils/errors.js +26 -0
- package/lib/esm/version.js +1 -1
- package/lib/types/common/factory.d.ts.map +1 -1
- package/lib/types/common/payment.d.ts +25 -13
- package/lib/types/common/payment.d.ts.map +1 -1
- package/lib/types/common/token/spl.d.ts +13 -0
- package/lib/types/common/token/spl.d.ts.map +1 -1
- package/lib/types/common/turbo.d.ts +18 -14
- package/lib/types/common/turbo.d.ts.map +1 -1
- package/lib/types/types.d.ts +45 -15
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/utils/errors.d.ts +24 -0
- package/lib/types/utils/errors.d.ts.map +1 -1
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -126,7 +126,7 @@ class TurboBaseFactory {
|
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
signerFromAdapter(walletAdapter, token) {
|
|
129
|
-
if (token
|
|
129
|
+
if (types_js_1.supportedSolanaSignerTokens.has(token)) {
|
|
130
130
|
if (!(0, types_js_1.isSolanaWalletAdapter)(walletAdapter)) {
|
|
131
131
|
throw new Error('Unsupported wallet adapter -- must implement publicKey and signMessage');
|
|
132
132
|
}
|
|
@@ -28,6 +28,34 @@ const logger_js_1 = require("./logger.js");
|
|
|
28
28
|
const index_js_1 = require("./token/index.js");
|
|
29
29
|
exports.developmentPaymentServiceURL = 'https://payment.services.ar-io.dev';
|
|
30
30
|
exports.defaultPaymentServiceURL = 'https://payment.ardrive.io';
|
|
31
|
+
/**
|
|
32
|
+
* Translate a `/sign` failure into the error a caller can act on, matching
|
|
33
|
+
* the payment service's own response bodies.
|
|
34
|
+
*
|
|
35
|
+
* - 402 → {@link InsufficientCreditsError}, as `createArNSAction` maps it.
|
|
36
|
+
* - 409 → {@link ArNSActionExpiredError}: the chain confirmed the signed
|
|
37
|
+
* bytes can never land. The body says whether the credits came back.
|
|
38
|
+
* - 400 `Action <nonce> expired …` → {@link ArNSActionExpiredError}. Matched
|
|
39
|
+
* on THIS nonce, because the same status carries on-chain program
|
|
40
|
+
* rejections ("Lease has expired", "Record is expired") that are not about
|
|
41
|
+
* the action at all.
|
|
42
|
+
* - Anything else — including 503 "Blockhash not found", which the service
|
|
43
|
+
* returns only when it cannot PROVE expiry — is returned unchanged.
|
|
44
|
+
*/
|
|
45
|
+
function arNSSignError(nonce, error) {
|
|
46
|
+
if (!(error instanceof errors_js_1.FailedRequestError))
|
|
47
|
+
return error;
|
|
48
|
+
const { status, message } = error;
|
|
49
|
+
if (status === 402)
|
|
50
|
+
return new errors_js_1.InsufficientCreditsError(message);
|
|
51
|
+
if (status === 409 && /expired/i.test(message)) {
|
|
52
|
+
return new errors_js_1.ArNSActionExpiredError(nonce, status, /credits have been returned/i.test(message), message);
|
|
53
|
+
}
|
|
54
|
+
if (status === 400 && message.includes(`Action ${nonce} expired`)) {
|
|
55
|
+
return new errors_js_1.ArNSActionExpiredError(nonce, status, /expired and was refunded/i.test(message), message);
|
|
56
|
+
}
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
31
59
|
class TurboUnauthenticatedPaymentService {
|
|
32
60
|
constructor({ url = exports.defaultPaymentServiceURL, logger = logger_js_1.Logger.default, retryConfig = (0, http_js_1.defaultRetryConfig)(logger), token = 'arweave', }) {
|
|
33
61
|
this.logger = logger;
|
|
@@ -607,17 +635,34 @@ class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymentServic
|
|
|
607
635
|
* the signature. Replaying a completed action returns `alreadyCompleted:
|
|
608
636
|
* true` rather than buying twice, so this is safe to call again if a
|
|
609
637
|
* response is lost.
|
|
638
|
+
*
|
|
639
|
+
* The route is authorised by the ANT owner's signature INSIDE the
|
|
640
|
+
* transaction, not by the payer: the credits were debited when the action
|
|
641
|
+
* was created. So no payer signature is sent by default — for a browser
|
|
642
|
+
* payer that would be a second wallet prompt inside the ~30 s blockhash
|
|
643
|
+
* window. Pass `headers` only for a service that requires them.
|
|
644
|
+
*
|
|
645
|
+
* Failures: {@link ArNSActionExpiredError} when the action or its
|
|
646
|
+
* transaction has provably expired (create a new action);
|
|
647
|
+
* {@link InsufficientCreditsError} on 402. A 503 "Blockhash not found"
|
|
648
|
+
* means expiry could not be proven — re-posting the SAME signed bytes is
|
|
649
|
+
* idempotent and may still succeed; do not re-create the action.
|
|
610
650
|
*/
|
|
611
|
-
async signArNSAction(nonce, signedTransaction) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
651
|
+
async signArNSAction(nonce, signedTransaction, headers) {
|
|
652
|
+
try {
|
|
653
|
+
return await this.httpService.post({
|
|
654
|
+
endpoint: `/arns/actions/${nonce}/sign`,
|
|
655
|
+
headers: {
|
|
656
|
+
...(headers ?? {}),
|
|
657
|
+
'content-type': 'application/json',
|
|
658
|
+
},
|
|
659
|
+
data: Buffer.from(JSON.stringify({ transaction: signedTransaction })),
|
|
660
|
+
retry: false,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
throw arNSSignError(nonce, error);
|
|
665
|
+
}
|
|
621
666
|
}
|
|
622
667
|
/**
|
|
623
668
|
* Status of an action by nonce. Open — no signature required — so it works
|
|
@@ -644,7 +689,7 @@ class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymentServic
|
|
|
644
689
|
: undefined);
|
|
645
690
|
// Fires before any wallet prompt: the action is already debited, so the
|
|
646
691
|
// caller needs the nonce persisted even if the user walks away here.
|
|
647
|
-
await opts.onNonce?.(created.nonce);
|
|
692
|
+
await opts.onNonce?.(created.nonce, created);
|
|
648
693
|
if (created.status === 'completed')
|
|
649
694
|
return created;
|
|
650
695
|
if (owner === undefined) {
|
|
@@ -653,6 +698,8 @@ class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymentServic
|
|
|
653
698
|
`Nonce ${created.nonce} is already debited — poll it rather than re-creating.`);
|
|
654
699
|
}
|
|
655
700
|
const signed = await owner.signTransaction(created.transaction);
|
|
701
|
+
// The owner's signature is the only prompt between create and submit:
|
|
702
|
+
// `/sign` needs no payer signature (see `signArNSAction`).
|
|
656
703
|
return this.signArNSAction(created.nonce, signed);
|
|
657
704
|
}
|
|
658
705
|
/**
|
|
@@ -23,6 +23,7 @@ const spl_token_1 = require("@solana/spl-token");
|
|
|
23
23
|
const web3_js_1 = require("@solana/web3.js");
|
|
24
24
|
const bignumber_js_1 = require("bignumber.js");
|
|
25
25
|
const bs58_1 = __importDefault(require("bs58"));
|
|
26
|
+
const types_js_1 = require("../../types.js");
|
|
26
27
|
const common_js_1 = require("../../utils/common.js");
|
|
27
28
|
const logger_js_1 = require("../logger.js");
|
|
28
29
|
const solana_js_1 = require("./solana.js");
|
|
@@ -80,11 +81,12 @@ class SplToken {
|
|
|
80
81
|
data: Buffer.from('turboCreditDestinationAddress=' + turboCreditDestinationAddress),
|
|
81
82
|
}));
|
|
82
83
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const { signedTx, signature } = signer.walletAdapter !== undefined &&
|
|
85
|
+
(0, types_js_1.isSolanaWalletAdapter)(signer.walletAdapter)
|
|
86
|
+
? await this.signWithWallet(tx, signer.walletAdapter)
|
|
87
|
+
: await this.signWithKey(tx, ownerPublicKey, signer);
|
|
86
88
|
const txId = bs58_1.default.encode(signature);
|
|
87
|
-
await this.submitTx(
|
|
89
|
+
await this.submitTx(signedTx, txId);
|
|
88
90
|
this.logger.debug(`Submitted ${this.tokenName} SPL transfer transaction...`, {
|
|
89
91
|
id: txId,
|
|
90
92
|
target,
|
|
@@ -95,6 +97,32 @@ class SplToken {
|
|
|
95
97
|
});
|
|
96
98
|
return { id: txId, target, reward: '0' };
|
|
97
99
|
}
|
|
100
|
+
/** Sign the message bytes directly: a private-key signer holds the key. */
|
|
101
|
+
async signWithKey(tx, ownerPublicKey, signer) {
|
|
102
|
+
const signature = await signer.signData(Uint8Array.from(tx.serializeMessage()));
|
|
103
|
+
tx.addSignature(ownerPublicKey, Buffer.from(signature));
|
|
104
|
+
return { signedTx: tx, signature };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Have a browser wallet sign the transaction itself, as the SOL path does.
|
|
108
|
+
*
|
|
109
|
+
* A wallet signs through `signTransaction`, not `signMessage`: it refuses, or
|
|
110
|
+
* cannot display, a serialized transaction handed to it as a message. It may
|
|
111
|
+
* also add instructions of its own before signing (compute budget, guards),
|
|
112
|
+
* so the transaction submitted is the one it returns, never ours with its
|
|
113
|
+
* signature attached. Deserializing drops `lastValidBlockHeight`, so that is
|
|
114
|
+
* carried over for confirmation.
|
|
115
|
+
*/
|
|
116
|
+
async signWithWallet(tx, walletAdapter) {
|
|
117
|
+
const signedTx = await walletAdapter.signTransaction(tx);
|
|
118
|
+
const signature = signedTx.signature;
|
|
119
|
+
if (signature === null) {
|
|
120
|
+
throw new Error(`Wallet returned an unsigned ${this.tokenName} transfer transaction`);
|
|
121
|
+
}
|
|
122
|
+
signedTx.recentBlockhash ??= tx.recentBlockhash;
|
|
123
|
+
signedTx.lastValidBlockHeight ??= tx.lastValidBlockHeight;
|
|
124
|
+
return { signedTx, signature: Uint8Array.from(signature) };
|
|
125
|
+
}
|
|
98
126
|
async submitTx(tx, id) {
|
|
99
127
|
this.logger.debug(`Submitting ${this.tokenName} fund transaction...`, {
|
|
100
128
|
id,
|
package/lib/cjs/common/turbo.js
CHANGED
|
@@ -245,9 +245,13 @@ class TurboAuthenticatedClient extends TurboUnauthenticatedClient {
|
|
|
245
245
|
createArNSAction(action, params, ownerProof) {
|
|
246
246
|
return this.paymentService.createArNSAction(action, params, ownerProof);
|
|
247
247
|
}
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Submit the owner-signed transaction (FULL serialized tx, base64).
|
|
250
|
+
* Authorised by the owner's signature inside the transaction; payer
|
|
251
|
+
* `headers` are optional, for services that require them.
|
|
252
|
+
*/
|
|
253
|
+
signArNSAction(nonce, signedTransaction, headers) {
|
|
254
|
+
return this.paymentService.signArNSAction(nonce, signedTransaction, headers);
|
|
251
255
|
}
|
|
252
256
|
/** Status by nonce — open, needs no signature. Use it to resume. */
|
|
253
257
|
getArNSActionStatus(nonce) {
|
package/lib/cjs/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.arNSFiatPurchaseMethods = exports.arNSActions = exports.arNSPurchaseIntents = exports.validChunkingModes = exports.isJWK = exports.isWebUploadFolderParams = exports.isNodeUploadFolderParams = exports.multipartFinalizedStatus = exports.multipartFailedStatus = exports.multipartPendingStatus = exports.X402Funding = exports.OnDemandFunding = exports.ExistingBalanceFunding = exports.supportedEvmSignerTokens = exports.tokenTypes = exports.fiatCurrencyTypes = void 0;
|
|
3
|
+
exports.arNSFiatPurchaseMethods = exports.arNSActions = exports.arNSPurchaseIntents = exports.validChunkingModes = exports.isJWK = exports.isWebUploadFolderParams = exports.isNodeUploadFolderParams = exports.multipartFinalizedStatus = exports.multipartFailedStatus = exports.multipartPendingStatus = exports.X402Funding = exports.OnDemandFunding = exports.ExistingBalanceFunding = exports.supportedEvmSignerTokens = exports.supportedSolanaSignerTokens = exports.tokenTypes = exports.fiatCurrencyTypes = void 0;
|
|
4
4
|
exports.isCurrency = isCurrency;
|
|
5
5
|
exports.isEthPrivateKey = isEthPrivateKey;
|
|
6
6
|
exports.isSolanaWalletAdapter = isSolanaWalletAdapter;
|
|
@@ -35,6 +35,16 @@ exports.tokenTypes = [
|
|
|
35
35
|
// USDC (SPL) on Solana — signed with the Solana signer, not an EVM one.
|
|
36
36
|
'solana-usdc',
|
|
37
37
|
];
|
|
38
|
+
/**
|
|
39
|
+
* Tokens paid from a Solana wallet: SOL natively, and ARIO and USDC as SPL
|
|
40
|
+
* transfers signed by the same ed25519 key. A private key for any of these
|
|
41
|
+
* builds a Solana signer, so a Solana wallet adapter must as well.
|
|
42
|
+
*/
|
|
43
|
+
exports.supportedSolanaSignerTokens = new Set([
|
|
44
|
+
'solana',
|
|
45
|
+
'ario',
|
|
46
|
+
'solana-usdc',
|
|
47
|
+
]);
|
|
38
48
|
exports.supportedEvmSignerTokens = new Set([
|
|
39
49
|
'ethereum',
|
|
40
50
|
'base-eth',
|
package/lib/cjs/utils/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AbortError = exports.FiatPaymentsDisabledError = exports.InsufficientCreditsError = exports.ProvidedInputError = exports.FailedRequestError = exports.UnauthenticatedRequestError = exports.BaseError = void 0;
|
|
3
|
+
exports.AbortError = exports.ArNSActionExpiredError = exports.FiatPaymentsDisabledError = exports.InsufficientCreditsError = exports.ProvidedInputError = exports.FailedRequestError = exports.UnauthenticatedRequestError = exports.BaseError = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
6
6
|
*
|
|
@@ -79,6 +79,33 @@ class FiatPaymentsDisabledError extends BaseError {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
exports.FiatPaymentsDisabledError = FiatPaymentsDisabledError;
|
|
82
|
+
/**
|
|
83
|
+
* Raised when an ArNS action's owner-signed transaction reaches `/sign` too
|
|
84
|
+
* late. Solana accepts the transaction for only about 30 seconds after Turbo
|
|
85
|
+
* builds it; signing again cannot help, because that transaction is dead.
|
|
86
|
+
*
|
|
87
|
+
* `creditsReleased` says whether the credits debited at creation are back:
|
|
88
|
+
* - 409: the chain confirmed the bytes can never land. `true` when the
|
|
89
|
+
* service says "Your credits have been returned"; `false` means its
|
|
90
|
+
* immediate refund did not go through and the reconciler will refund.
|
|
91
|
+
* - 400 `Action <nonce> expired …`: `true` for "expired and was refunded";
|
|
92
|
+
* `false` for the variant refused before submission, refunded
|
|
93
|
+
* automatically once the reservation (`expiresAt`, ~15 min) lapses.
|
|
94
|
+
*
|
|
95
|
+
* Extends {@link FailedRequestError}, so existing `status` checks still work.
|
|
96
|
+
* Recovery: create a NEW action if the change is still wanted.
|
|
97
|
+
*/
|
|
98
|
+
class ArNSActionExpiredError extends FailedRequestError {
|
|
99
|
+
constructor(nonce, status, creditsReleased,
|
|
100
|
+
/** The service's response body, or a `FailedRequestError` message. */
|
|
101
|
+
message) {
|
|
102
|
+
super(message?.replace(/^Failed request \(Status \d+\): /, '') ??
|
|
103
|
+
`ArNS action ${nonce} expired before its signed transaction was submitted.`, status);
|
|
104
|
+
this.nonce = nonce;
|
|
105
|
+
this.creditsReleased = creditsReleased;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.ArNSActionExpiredError = ArNSActionExpiredError;
|
|
82
109
|
class AbortError extends BaseError {
|
|
83
110
|
constructor(message = 'Request was aborted') {
|
|
84
111
|
super(message);
|
package/lib/cjs/version.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { HexInjectedSolanaSigner, InjectedEthereumSigner, SignatureConfig, } from '@dha-team/arbundles';
|
|
17
|
-
import { isEthereumWalletAdapter, isSolanaWalletAdapter, supportedEvmSignerTokens, } from '../types.js';
|
|
17
|
+
import { isEthereumWalletAdapter, isSolanaWalletAdapter, supportedEvmSignerTokens, supportedSolanaSignerTokens, } from '../types.js';
|
|
18
18
|
import { Logger } from './logger.js';
|
|
19
19
|
import { TurboAuthenticatedPaymentService, TurboUnauthenticatedPaymentService, } from './payment.js';
|
|
20
20
|
import { defaultTokenMap } from './token/index.js';
|
|
@@ -123,7 +123,7 @@ export class TurboBaseFactory {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
signerFromAdapter(walletAdapter, token) {
|
|
126
|
-
if (token
|
|
126
|
+
if (supportedSolanaSignerTokens.has(token)) {
|
|
127
127
|
if (!isSolanaWalletAdapter(walletAdapter)) {
|
|
128
128
|
throw new Error('Unsupported wallet adapter -- must implement publicKey and signMessage');
|
|
129
129
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { BigNumber } from 'bignumber.js';
|
|
17
17
|
import { arNSPurchaseIntents, fiatCurrencyTypes, isCurrency, } from '../types.js';
|
|
18
18
|
import { isAnyValidUserAddress, isValidArweaveBase64URL, isValidSolanaAddress, } from '../utils/common.js';
|
|
19
|
-
import { FailedRequestError, FiatPaymentsDisabledError, InsufficientCreditsError, ProvidedInputError, } from '../utils/errors.js';
|
|
19
|
+
import { ArNSActionExpiredError, FailedRequestError, FiatPaymentsDisabledError, InsufficientCreditsError, ProvidedInputError, } from '../utils/errors.js';
|
|
20
20
|
import { uuidV4 } from '../utils/uuid.js';
|
|
21
21
|
import { arNSKeywordsField, arNSMetadataField, arNSOwnerProofHeaders, buildArNSCustodyMessage, } from './arnsActions.js';
|
|
22
22
|
import { defaultRetryConfig } from './http.js';
|
|
@@ -25,6 +25,34 @@ import { Logger } from './logger.js';
|
|
|
25
25
|
import { exponentMap, tokenToBaseMap } from './token/index.js';
|
|
26
26
|
export const developmentPaymentServiceURL = 'https://payment.services.ar-io.dev';
|
|
27
27
|
export const defaultPaymentServiceURL = 'https://payment.ardrive.io';
|
|
28
|
+
/**
|
|
29
|
+
* Translate a `/sign` failure into the error a caller can act on, matching
|
|
30
|
+
* the payment service's own response bodies.
|
|
31
|
+
*
|
|
32
|
+
* - 402 → {@link InsufficientCreditsError}, as `createArNSAction` maps it.
|
|
33
|
+
* - 409 → {@link ArNSActionExpiredError}: the chain confirmed the signed
|
|
34
|
+
* bytes can never land. The body says whether the credits came back.
|
|
35
|
+
* - 400 `Action <nonce> expired …` → {@link ArNSActionExpiredError}. Matched
|
|
36
|
+
* on THIS nonce, because the same status carries on-chain program
|
|
37
|
+
* rejections ("Lease has expired", "Record is expired") that are not about
|
|
38
|
+
* the action at all.
|
|
39
|
+
* - Anything else — including 503 "Blockhash not found", which the service
|
|
40
|
+
* returns only when it cannot PROVE expiry — is returned unchanged.
|
|
41
|
+
*/
|
|
42
|
+
function arNSSignError(nonce, error) {
|
|
43
|
+
if (!(error instanceof FailedRequestError))
|
|
44
|
+
return error;
|
|
45
|
+
const { status, message } = error;
|
|
46
|
+
if (status === 402)
|
|
47
|
+
return new InsufficientCreditsError(message);
|
|
48
|
+
if (status === 409 && /expired/i.test(message)) {
|
|
49
|
+
return new ArNSActionExpiredError(nonce, status, /credits have been returned/i.test(message), message);
|
|
50
|
+
}
|
|
51
|
+
if (status === 400 && message.includes(`Action ${nonce} expired`)) {
|
|
52
|
+
return new ArNSActionExpiredError(nonce, status, /expired and was refunded/i.test(message), message);
|
|
53
|
+
}
|
|
54
|
+
return error;
|
|
55
|
+
}
|
|
28
56
|
export class TurboUnauthenticatedPaymentService {
|
|
29
57
|
constructor({ url = defaultPaymentServiceURL, logger = Logger.default, retryConfig = defaultRetryConfig(logger), token = 'arweave', }) {
|
|
30
58
|
this.logger = logger;
|
|
@@ -603,17 +631,34 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
|
|
|
603
631
|
* the signature. Replaying a completed action returns `alreadyCompleted:
|
|
604
632
|
* true` rather than buying twice, so this is safe to call again if a
|
|
605
633
|
* response is lost.
|
|
634
|
+
*
|
|
635
|
+
* The route is authorised by the ANT owner's signature INSIDE the
|
|
636
|
+
* transaction, not by the payer: the credits were debited when the action
|
|
637
|
+
* was created. So no payer signature is sent by default — for a browser
|
|
638
|
+
* payer that would be a second wallet prompt inside the ~30 s blockhash
|
|
639
|
+
* window. Pass `headers` only for a service that requires them.
|
|
640
|
+
*
|
|
641
|
+
* Failures: {@link ArNSActionExpiredError} when the action or its
|
|
642
|
+
* transaction has provably expired (create a new action);
|
|
643
|
+
* {@link InsufficientCreditsError} on 402. A 503 "Blockhash not found"
|
|
644
|
+
* means expiry could not be proven — re-posting the SAME signed bytes is
|
|
645
|
+
* idempotent and may still succeed; do not re-create the action.
|
|
606
646
|
*/
|
|
607
|
-
async signArNSAction(nonce, signedTransaction) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
647
|
+
async signArNSAction(nonce, signedTransaction, headers) {
|
|
648
|
+
try {
|
|
649
|
+
return await this.httpService.post({
|
|
650
|
+
endpoint: `/arns/actions/${nonce}/sign`,
|
|
651
|
+
headers: {
|
|
652
|
+
...(headers ?? {}),
|
|
653
|
+
'content-type': 'application/json',
|
|
654
|
+
},
|
|
655
|
+
data: Buffer.from(JSON.stringify({ transaction: signedTransaction })),
|
|
656
|
+
retry: false,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
throw arNSSignError(nonce, error);
|
|
661
|
+
}
|
|
617
662
|
}
|
|
618
663
|
/**
|
|
619
664
|
* Status of an action by nonce. Open — no signature required — so it works
|
|
@@ -640,7 +685,7 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
|
|
|
640
685
|
: undefined);
|
|
641
686
|
// Fires before any wallet prompt: the action is already debited, so the
|
|
642
687
|
// caller needs the nonce persisted even if the user walks away here.
|
|
643
|
-
await opts.onNonce?.(created.nonce);
|
|
688
|
+
await opts.onNonce?.(created.nonce, created);
|
|
644
689
|
if (created.status === 'completed')
|
|
645
690
|
return created;
|
|
646
691
|
if (owner === undefined) {
|
|
@@ -649,6 +694,8 @@ export class TurboAuthenticatedPaymentService extends TurboUnauthenticatedPaymen
|
|
|
649
694
|
`Nonce ${created.nonce} is already debited — poll it rather than re-creating.`);
|
|
650
695
|
}
|
|
651
696
|
const signed = await owner.signTransaction(created.transaction);
|
|
697
|
+
// The owner's signature is the only prompt between create and submit:
|
|
698
|
+
// `/sign` needs no payer signature (see `signArNSAction`).
|
|
652
699
|
return this.signArNSAction(created.nonce, signed);
|
|
653
700
|
}
|
|
654
701
|
/**
|
|
@@ -17,6 +17,7 @@ import { createAssociatedTokenAccountIdempotentInstruction, createTransferChecke
|
|
|
17
17
|
import { Connection, PublicKey, Transaction, TransactionInstruction, } from '@solana/web3.js';
|
|
18
18
|
import { BigNumber } from 'bignumber.js';
|
|
19
19
|
import bs58 from 'bs58';
|
|
20
|
+
import { isSolanaWalletAdapter, } from '../../types.js';
|
|
20
21
|
import { defaultProdGatewayUrls, sleep } from '../../utils/common.js';
|
|
21
22
|
import { Logger } from '../logger.js';
|
|
22
23
|
import { memoProgramId } from './solana.js';
|
|
@@ -74,11 +75,12 @@ export class SplToken {
|
|
|
74
75
|
data: Buffer.from('turboCreditDestinationAddress=' + turboCreditDestinationAddress),
|
|
75
76
|
}));
|
|
76
77
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const { signedTx, signature } = signer.walletAdapter !== undefined &&
|
|
79
|
+
isSolanaWalletAdapter(signer.walletAdapter)
|
|
80
|
+
? await this.signWithWallet(tx, signer.walletAdapter)
|
|
81
|
+
: await this.signWithKey(tx, ownerPublicKey, signer);
|
|
80
82
|
const txId = bs58.encode(signature);
|
|
81
|
-
await this.submitTx(
|
|
83
|
+
await this.submitTx(signedTx, txId);
|
|
82
84
|
this.logger.debug(`Submitted ${this.tokenName} SPL transfer transaction...`, {
|
|
83
85
|
id: txId,
|
|
84
86
|
target,
|
|
@@ -89,6 +91,32 @@ export class SplToken {
|
|
|
89
91
|
});
|
|
90
92
|
return { id: txId, target, reward: '0' };
|
|
91
93
|
}
|
|
94
|
+
/** Sign the message bytes directly: a private-key signer holds the key. */
|
|
95
|
+
async signWithKey(tx, ownerPublicKey, signer) {
|
|
96
|
+
const signature = await signer.signData(Uint8Array.from(tx.serializeMessage()));
|
|
97
|
+
tx.addSignature(ownerPublicKey, Buffer.from(signature));
|
|
98
|
+
return { signedTx: tx, signature };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Have a browser wallet sign the transaction itself, as the SOL path does.
|
|
102
|
+
*
|
|
103
|
+
* A wallet signs through `signTransaction`, not `signMessage`: it refuses, or
|
|
104
|
+
* cannot display, a serialized transaction handed to it as a message. It may
|
|
105
|
+
* also add instructions of its own before signing (compute budget, guards),
|
|
106
|
+
* so the transaction submitted is the one it returns, never ours with its
|
|
107
|
+
* signature attached. Deserializing drops `lastValidBlockHeight`, so that is
|
|
108
|
+
* carried over for confirmation.
|
|
109
|
+
*/
|
|
110
|
+
async signWithWallet(tx, walletAdapter) {
|
|
111
|
+
const signedTx = await walletAdapter.signTransaction(tx);
|
|
112
|
+
const signature = signedTx.signature;
|
|
113
|
+
if (signature === null) {
|
|
114
|
+
throw new Error(`Wallet returned an unsigned ${this.tokenName} transfer transaction`);
|
|
115
|
+
}
|
|
116
|
+
signedTx.recentBlockhash ??= tx.recentBlockhash;
|
|
117
|
+
signedTx.lastValidBlockHeight ??= tx.lastValidBlockHeight;
|
|
118
|
+
return { signedTx, signature: Uint8Array.from(signature) };
|
|
119
|
+
}
|
|
92
120
|
async submitTx(tx, id) {
|
|
93
121
|
this.logger.debug(`Submitting ${this.tokenName} fund transaction...`, {
|
|
94
122
|
id,
|
package/lib/esm/common/turbo.js
CHANGED
|
@@ -241,9 +241,13 @@ export class TurboAuthenticatedClient extends TurboUnauthenticatedClient {
|
|
|
241
241
|
createArNSAction(action, params, ownerProof) {
|
|
242
242
|
return this.paymentService.createArNSAction(action, params, ownerProof);
|
|
243
243
|
}
|
|
244
|
-
/**
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Submit the owner-signed transaction (FULL serialized tx, base64).
|
|
246
|
+
* Authorised by the owner's signature inside the transaction; payer
|
|
247
|
+
* `headers` are optional, for services that require them.
|
|
248
|
+
*/
|
|
249
|
+
signArNSAction(nonce, signedTransaction, headers) {
|
|
250
|
+
return this.paymentService.signArNSAction(nonce, signedTransaction, headers);
|
|
247
251
|
}
|
|
248
252
|
/** Status by nonce — open, needs no signature. Use it to resume. */
|
|
249
253
|
getArNSActionStatus(nonce) {
|
package/lib/esm/types.js
CHANGED
|
@@ -28,6 +28,16 @@ export const tokenTypes = [
|
|
|
28
28
|
// USDC (SPL) on Solana — signed with the Solana signer, not an EVM one.
|
|
29
29
|
'solana-usdc',
|
|
30
30
|
];
|
|
31
|
+
/**
|
|
32
|
+
* Tokens paid from a Solana wallet: SOL natively, and ARIO and USDC as SPL
|
|
33
|
+
* transfers signed by the same ed25519 key. A private key for any of these
|
|
34
|
+
* builds a Solana signer, so a Solana wallet adapter must as well.
|
|
35
|
+
*/
|
|
36
|
+
export const supportedSolanaSignerTokens = new Set([
|
|
37
|
+
'solana',
|
|
38
|
+
'ario',
|
|
39
|
+
'solana-usdc',
|
|
40
|
+
]);
|
|
31
41
|
export const supportedEvmSignerTokens = new Set([
|
|
32
42
|
'ethereum',
|
|
33
43
|
'base-eth',
|
package/lib/esm/utils/errors.js
CHANGED
|
@@ -70,6 +70,32 @@ export class FiatPaymentsDisabledError extends BaseError {
|
|
|
70
70
|
this.status = 503;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Raised when an ArNS action's owner-signed transaction reaches `/sign` too
|
|
75
|
+
* late. Solana accepts the transaction for only about 30 seconds after Turbo
|
|
76
|
+
* builds it; signing again cannot help, because that transaction is dead.
|
|
77
|
+
*
|
|
78
|
+
* `creditsReleased` says whether the credits debited at creation are back:
|
|
79
|
+
* - 409: the chain confirmed the bytes can never land. `true` when the
|
|
80
|
+
* service says "Your credits have been returned"; `false` means its
|
|
81
|
+
* immediate refund did not go through and the reconciler will refund.
|
|
82
|
+
* - 400 `Action <nonce> expired …`: `true` for "expired and was refunded";
|
|
83
|
+
* `false` for the variant refused before submission, refunded
|
|
84
|
+
* automatically once the reservation (`expiresAt`, ~15 min) lapses.
|
|
85
|
+
*
|
|
86
|
+
* Extends {@link FailedRequestError}, so existing `status` checks still work.
|
|
87
|
+
* Recovery: create a NEW action if the change is still wanted.
|
|
88
|
+
*/
|
|
89
|
+
export class ArNSActionExpiredError extends FailedRequestError {
|
|
90
|
+
constructor(nonce, status, creditsReleased,
|
|
91
|
+
/** The service's response body, or a `FailedRequestError` message. */
|
|
92
|
+
message) {
|
|
93
|
+
super(message?.replace(/^Failed request \(Status \d+\): /, '') ??
|
|
94
|
+
`ArNS action ${nonce} expired before its signed transaction was submitted.`, status);
|
|
95
|
+
this.nonce = nonce;
|
|
96
|
+
this.creditsReleased = creditsReleased;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
73
99
|
export class AbortError extends BaseError {
|
|
74
100
|
constructor(message = 'Request was aborted') {
|
|
75
101
|
super(message);
|
package/lib/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/common/factory.ts"],"names":[],"mappings":"AAqBA,OAAO,EACL,oBAAoB,EAEpB,+BAA+B,EAC/B,4CAA4C,EAC5C,wCAAwC,EACxC,iCAAiC,
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/common/factory.ts"],"names":[],"mappings":"AAqBA,OAAO,EACL,oBAAoB,EAEpB,+BAA+B,EAC/B,4CAA4C,EAC5C,wCAAwC,EACxC,iCAAiC,EAMlC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EACL,gCAAgC,EAEjC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EACL,wBAAwB,EACxB,0BAA0B,EAC3B,MAAM,YAAY,CAAC;AAGpB,8BAAsB,gBAAgB;IACpC,SAAS,CAAC,MAAM,CAAC,MAAM,SAAkB;IAGzC,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,QAAQ;IAIlC,MAAM,CAAC,eAAe,CAAC,EACrB,oBAAyB,EACzB,mBAAwB,EACxB,KAAK,GACN,GAAE,iCAAsC;IAqBzC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,EAC3B,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,MAAM,EACN,KAAK,GACN,EAAE,oBAAoB,GAAG,2BAA2B;IAErD,SAAS,CAAC,QAAQ,CAAC,6BAA6B,CAC9C,MAAM,EAAE,4CAA4C,GAAG;QACrD,cAAc,EAAE,gCAAgC,CAAC;KAClD,GACA,wCAAwC;IAE3C,SAAS,CAAC,qBAAqB,CAAC,EAC9B,UAAU,EACV,MAAM,EAAE,cAAc,EACtB,oBAAyB,EACzB,mBAAwB,EACxB,KAAK,EACL,UAAU,EACV,QAAQ,EACR,UAAU,EACV,MAAM,EACN,aAAa,GACd,EAAE,+BAA+B,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE;IA0FvD,OAAO,CAAC,iBAAiB;CAuB1B"}
|