@ar.io/sdk 3.10.0 → 3.11.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -125
- package/bundles/web.bundle.min.js +130 -125
- package/lib/cjs/cli/options.js +12 -0
- package/lib/cjs/cli/utils.js +32 -10
- package/lib/cjs/common/ant-versions.js +5 -5
- package/lib/cjs/common/index.js +2 -0
- package/lib/cjs/common/io.js +64 -1
- package/lib/cjs/common/turbo.js +208 -0
- package/lib/cjs/common/wayfinder/gateways.js +75 -0
- package/lib/cjs/common/wayfinder/index.js +36 -0
- package/lib/cjs/common/wayfinder/routers/fixed.js +14 -0
- package/lib/cjs/common/wayfinder/routers/fixed.test.js +14 -0
- package/lib/cjs/common/wayfinder/routers/priority.js +38 -0
- package/lib/cjs/common/wayfinder/routers/priority.test.js +155 -0
- package/lib/cjs/common/wayfinder/routers/random.js +23 -0
- package/lib/cjs/common/wayfinder/routers/random.test.js +104 -0
- package/lib/cjs/common/wayfinder/routers/simple-cache.js +25 -0
- package/lib/cjs/common/wayfinder/routers/simple-cache.test.js +41 -0
- package/lib/cjs/common/wayfinder/wayfinder.js +167 -0
- package/lib/cjs/common/wayfinder/wayfinder.test.js +253 -0
- package/lib/cjs/types/ant.js +12 -1
- package/lib/cjs/types/index.js +1 -0
- package/lib/cjs/types/io.js +1 -1
- package/lib/cjs/types/wayfinder.js +2 -0
- package/lib/cjs/utils/ao.js +5 -0
- package/lib/cjs/utils/random.js +30 -0
- package/lib/cjs/utils/url.js +28 -0
- package/lib/cjs/utils/url.test.js +24 -0
- package/lib/cjs/utils/utils.test.js +8 -8
- package/lib/cjs/version.js +1 -1
- package/lib/cjs/web/index.js +3 -1
- package/lib/esm/cli/options.js +12 -0
- package/lib/esm/cli/utils.js +31 -10
- package/lib/esm/common/ant-versions.js +5 -5
- package/lib/esm/common/index.js +2 -0
- package/lib/esm/common/io.js +64 -1
- package/lib/esm/common/turbo.js +200 -0
- package/lib/esm/common/wayfinder/gateways.js +69 -0
- package/lib/esm/common/wayfinder/index.js +20 -0
- package/lib/esm/common/wayfinder/routers/fixed.js +10 -0
- package/lib/esm/common/wayfinder/routers/fixed.test.js +12 -0
- package/lib/esm/common/wayfinder/routers/priority.js +34 -0
- package/lib/esm/common/wayfinder/routers/priority.test.js +153 -0
- package/lib/esm/common/wayfinder/routers/random.js +19 -0
- package/lib/esm/common/wayfinder/routers/random.test.js +102 -0
- package/lib/esm/common/wayfinder/routers/simple-cache.js +21 -0
- package/lib/esm/common/wayfinder/routers/simple-cache.test.js +39 -0
- package/lib/esm/common/wayfinder/wayfinder.js +161 -0
- package/lib/esm/common/wayfinder/wayfinder.test.js +248 -0
- package/lib/esm/types/ant.js +11 -0
- package/lib/esm/types/index.js +1 -0
- package/lib/esm/types/io.js +1 -1
- package/lib/esm/types/wayfinder.js +1 -0
- package/lib/esm/utils/ao.js +5 -0
- package/lib/esm/utils/random.js +26 -0
- package/lib/esm/utils/url.js +24 -0
- package/lib/esm/utils/url.test.js +19 -0
- package/lib/esm/utils/utils.test.js +1 -1
- package/lib/esm/version.js +1 -1
- package/lib/esm/web/index.js +1 -1
- package/lib/types/cli/commands/antCommands.d.ts +3 -3
- package/lib/types/cli/commands/arnsPurchaseCommands.d.ts +1 -1
- package/lib/types/cli/commands/gatewayWriteCommands.d.ts +9 -9
- package/lib/types/cli/commands/readCommands.d.ts +1 -0
- package/lib/types/cli/commands/transfer.d.ts +3 -3
- package/lib/types/cli/options.d.ts +9 -0
- package/lib/types/cli/types.d.ts +3 -0
- package/lib/types/cli/utils.d.ts +4 -0
- package/lib/types/common/ant-versions.d.ts +3 -6
- package/lib/types/common/index.d.ts +1 -0
- package/lib/types/common/io.d.ts +9 -7
- package/lib/types/common/turbo.d.ts +62 -0
- package/lib/types/common/wayfinder/gateways.d.ts +44 -0
- package/lib/types/common/wayfinder/index.d.ts +19 -0
- package/lib/types/common/wayfinder/routers/fixed.d.ts +24 -0
- package/lib/types/common/wayfinder/routers/fixed.test.d.ts +1 -0
- package/lib/types/common/wayfinder/routers/priority.d.ts +33 -0
- package/lib/types/common/wayfinder/routers/priority.test.d.ts +1 -0
- package/lib/types/common/wayfinder/routers/random.d.ts +27 -0
- package/lib/types/common/wayfinder/routers/random.test.d.ts +1 -0
- package/lib/types/common/wayfinder/routers/simple-cache.d.ts +28 -0
- package/lib/types/common/wayfinder/routers/simple-cache.test.d.ts +1 -0
- package/lib/types/common/wayfinder/wayfinder.d.ts +113 -0
- package/lib/types/common/wayfinder/wayfinder.test.d.ts +1 -0
- package/lib/types/types/ant.d.ts +53 -1
- package/lib/types/types/common.d.ts +7 -2
- package/lib/types/types/index.d.ts +1 -0
- package/lib/types/types/io.d.ts +7 -5
- package/lib/types/types/wayfinder.d.ts +19 -0
- package/lib/types/utils/ao.d.ts +1 -12
- package/lib/types/utils/random.d.ts +7 -0
- package/lib/types/utils/url.d.ts +19 -0
- package/lib/types/utils/url.test.d.ts +1 -0
- package/lib/types/version.d.ts +1 -1
- package/lib/types/web/index.d.ts +1 -1
- package/package.json +5 -3
package/lib/esm/common/index.js
CHANGED
package/lib/esm/common/io.js
CHANGED
|
@@ -22,6 +22,7 @@ import { defaultArweave } from './arweave.js';
|
|
|
22
22
|
import { AOProcess } from './contracts/ao-process.js';
|
|
23
23
|
import { InvalidContractConfigurationError } from './error.js';
|
|
24
24
|
import { createFaucet } from './faucet.js';
|
|
25
|
+
import { TurboArNSPaymentFactory, TurboArNSPaymentProviderAuthenticated, isTurboArNSSigner, } from './turbo.js';
|
|
25
26
|
export class ARIO {
|
|
26
27
|
// Implementation
|
|
27
28
|
static init(config) {
|
|
@@ -85,6 +86,7 @@ export class ARIOReadable {
|
|
|
85
86
|
process;
|
|
86
87
|
epochSettings;
|
|
87
88
|
arweave;
|
|
89
|
+
paymentProvider; // TODO: this could be an array/map of payment providers
|
|
88
90
|
constructor(config) {
|
|
89
91
|
this.arweave = config?.arweave ?? defaultArweave;
|
|
90
92
|
if (config === undefined || Object.keys(config).length === 0) {
|
|
@@ -103,6 +105,9 @@ export class ARIOReadable {
|
|
|
103
105
|
else {
|
|
104
106
|
throw new InvalidContractConfigurationError();
|
|
105
107
|
}
|
|
108
|
+
this.paymentProvider = TurboArNSPaymentFactory.init({
|
|
109
|
+
paymentUrl: config?.paymentUrl,
|
|
110
|
+
});
|
|
106
111
|
}
|
|
107
112
|
async getInfo() {
|
|
108
113
|
return this.process.read({
|
|
@@ -434,6 +439,20 @@ export class ARIOReadable {
|
|
|
434
439
|
// TODO: Can overload this function to refine different types of cost details params
|
|
435
440
|
async getCostDetails({ intent, type, years, name, quantity, fromAddress, fundFrom, }) {
|
|
436
441
|
const replacedBuyRecordWithBuyName = intent === 'Buy-Record' ? 'Buy-Name' : intent;
|
|
442
|
+
if (fundFrom === 'turbo') {
|
|
443
|
+
const { mARIO, winc } = await this.paymentProvider.getArNSPriceDetails({
|
|
444
|
+
intent: replacedBuyRecordWithBuyName,
|
|
445
|
+
name,
|
|
446
|
+
quantity,
|
|
447
|
+
type,
|
|
448
|
+
years,
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
tokenCost: +mARIO,
|
|
452
|
+
wincQty: winc,
|
|
453
|
+
discounts: [],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
437
456
|
const allTags = [
|
|
438
457
|
{ name: 'Action', value: 'Cost-Details' },
|
|
439
458
|
{
|
|
@@ -600,7 +619,8 @@ export class ARIOReadable {
|
|
|
600
619
|
}
|
|
601
620
|
export class ARIOWriteable extends ARIOReadable {
|
|
602
621
|
signer;
|
|
603
|
-
|
|
622
|
+
paymentProvider;
|
|
623
|
+
constructor({ signer, paymentUrl, ...config }) {
|
|
604
624
|
if (config === undefined) {
|
|
605
625
|
super({
|
|
606
626
|
process: new AOProcess({
|
|
@@ -612,6 +632,10 @@ export class ARIOWriteable extends ARIOReadable {
|
|
|
612
632
|
super(config);
|
|
613
633
|
}
|
|
614
634
|
this.signer = createAoSigner(signer);
|
|
635
|
+
this.paymentProvider = TurboArNSPaymentFactory.init({
|
|
636
|
+
signer: isTurboArNSSigner(signer) ? signer : undefined,
|
|
637
|
+
paymentUrl,
|
|
638
|
+
});
|
|
615
639
|
}
|
|
616
640
|
async transfer({ target, qty, }, options) {
|
|
617
641
|
const { tags = [] } = options || {};
|
|
@@ -886,6 +910,15 @@ export class ARIOWriteable extends ARIOReadable {
|
|
|
886
910
|
});
|
|
887
911
|
}
|
|
888
912
|
async buyRecord(params, options) {
|
|
913
|
+
if (params.fundFrom === 'turbo') {
|
|
914
|
+
if (!(this.paymentProvider instanceof TurboArNSPaymentProviderAuthenticated)) {
|
|
915
|
+
throw new Error('Turbo funding is not supported for this payment provider');
|
|
916
|
+
}
|
|
917
|
+
return this.paymentProvider.initiateArNSPurchase({
|
|
918
|
+
intent: 'Buy-Name',
|
|
919
|
+
...params,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
889
922
|
const { tags = [] } = options || {};
|
|
890
923
|
const allTags = [
|
|
891
924
|
...tags,
|
|
@@ -910,6 +943,15 @@ export class ARIOWriteable extends ARIOReadable {
|
|
|
910
943
|
* @returns {Promise<AoMessageResult>} The result of the upgrade
|
|
911
944
|
*/
|
|
912
945
|
async upgradeRecord(params, options) {
|
|
946
|
+
if (params.fundFrom === 'turbo') {
|
|
947
|
+
if (!(this.paymentProvider instanceof TurboArNSPaymentProviderAuthenticated)) {
|
|
948
|
+
throw new Error('Turbo funding is not supported for this payment provider');
|
|
949
|
+
}
|
|
950
|
+
return this.paymentProvider.initiateArNSPurchase({
|
|
951
|
+
intent: 'Upgrade-Name',
|
|
952
|
+
name: params.name,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
913
955
|
const { tags = [] } = options || {};
|
|
914
956
|
const allTags = [
|
|
915
957
|
...tags,
|
|
@@ -932,6 +974,15 @@ export class ARIOWriteable extends ARIOReadable {
|
|
|
932
974
|
* @returns {Promise<AoMessageResult>} The result of the extension
|
|
933
975
|
*/
|
|
934
976
|
async extendLease(params, options) {
|
|
977
|
+
if (params.fundFrom === 'turbo') {
|
|
978
|
+
if (!(this.paymentProvider instanceof TurboArNSPaymentProviderAuthenticated)) {
|
|
979
|
+
throw new Error('Turbo funding is not supported for this payment provider');
|
|
980
|
+
}
|
|
981
|
+
return this.paymentProvider.initiateArNSPurchase({
|
|
982
|
+
intent: 'Extend-Lease',
|
|
983
|
+
...params,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
935
986
|
const { tags = [] } = options || {};
|
|
936
987
|
const allTags = [
|
|
937
988
|
...tags,
|
|
@@ -946,6 +997,15 @@ export class ARIOWriteable extends ARIOReadable {
|
|
|
946
997
|
});
|
|
947
998
|
}
|
|
948
999
|
async increaseUndernameLimit(params, options) {
|
|
1000
|
+
if (params.fundFrom === 'turbo') {
|
|
1001
|
+
if (!(this.paymentProvider instanceof TurboArNSPaymentProviderAuthenticated)) {
|
|
1002
|
+
throw new Error('Turbo funding is not supported for this payment provider');
|
|
1003
|
+
}
|
|
1004
|
+
return this.paymentProvider.initiateArNSPurchase({
|
|
1005
|
+
intent: 'Increase-Undername-Limit',
|
|
1006
|
+
...params,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
949
1009
|
const { tags = [] } = options || {};
|
|
950
1010
|
const allTags = [
|
|
951
1011
|
...tags,
|
|
@@ -982,6 +1042,9 @@ export class ARIOWriteable extends ARIOReadable {
|
|
|
982
1042
|
});
|
|
983
1043
|
}
|
|
984
1044
|
async requestPrimaryName(params, options) {
|
|
1045
|
+
if (params.fundFrom === 'turbo') {
|
|
1046
|
+
throw new Error('Turbo funding is not yet supported for primary name requests');
|
|
1047
|
+
}
|
|
985
1048
|
const { tags = [] } = options || {};
|
|
986
1049
|
const allTags = [
|
|
987
1050
|
...tags,
|
|
@@ -0,0 +1,200 @@
|
|
|
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 { ArconnectSigner, ArweaveSigner, EthereumSigner, InjectedEthereumSigner, SignatureConfig, } from '@dha-team/arbundles';
|
|
17
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
18
|
+
import { mARIOToken } from '../types/token.js';
|
|
19
|
+
import { toB64Url } from '../utils/base64.js';
|
|
20
|
+
import { createAxiosInstance } from '../utils/http-client.js';
|
|
21
|
+
import { urlWithSearchParams } from '../utils/url.js';
|
|
22
|
+
import { Logger } from './logger.js';
|
|
23
|
+
export async function signedRequestHeadersFromSigner({ signer, nonce = uuidv4(), }) {
|
|
24
|
+
let signature = undefined;
|
|
25
|
+
let publicKey = undefined;
|
|
26
|
+
const signatureType = isWanderArweaveBrowserSigner(signer)
|
|
27
|
+
? SignatureConfig.ARWEAVE
|
|
28
|
+
: signer.signatureType;
|
|
29
|
+
// equivalent to window.arweaveWallet
|
|
30
|
+
if (isWanderArweaveBrowserSigner(signer)) {
|
|
31
|
+
signature = toB64Url(Buffer.from(await signer.signMessage(Uint8Array.from(Buffer.from(nonce)))));
|
|
32
|
+
}
|
|
33
|
+
else if (signer instanceof ArconnectSigner) {
|
|
34
|
+
signature = toB64Url(Buffer.from(await signer['signer'].signMessage(Uint8Array.from(Buffer.from(nonce)))));
|
|
35
|
+
}
|
|
36
|
+
else if (signer instanceof ArweaveSigner ||
|
|
37
|
+
signer instanceof EthereumSigner ||
|
|
38
|
+
signer instanceof InjectedEthereumSigner) {
|
|
39
|
+
if ('setPublicKey' in signer && signer['publicKey'] === undefined) {
|
|
40
|
+
await signer.setPublicKey();
|
|
41
|
+
}
|
|
42
|
+
signature = toB64Url(Buffer.from(await signer.sign(Uint8Array.from(Buffer.from(nonce)))));
|
|
43
|
+
}
|
|
44
|
+
switch (signatureType) {
|
|
45
|
+
case SignatureConfig.ARWEAVE:
|
|
46
|
+
if (isWanderArweaveBrowserSigner(signer)) {
|
|
47
|
+
publicKey = await signer.getActivePublicKey();
|
|
48
|
+
}
|
|
49
|
+
else if ('setPublicKey' in signer) {
|
|
50
|
+
await signer.setPublicKey();
|
|
51
|
+
publicKey = toB64Url(signer.publicKey);
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case SignatureConfig.ETHEREUM:
|
|
55
|
+
if ('publicKey' in signer) {
|
|
56
|
+
publicKey = '0x' + signer.publicKey.toString('hex');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw new Error('Public key not found');
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
// TODO: solana sig support
|
|
63
|
+
// case SignatureConfig.SOLANA:
|
|
64
|
+
// case SignatureConfig.ED25519:
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(`Unsupported signer type for signing requests: ${signatureType}`);
|
|
67
|
+
}
|
|
68
|
+
if (publicKey === undefined || signature === undefined) {
|
|
69
|
+
throw new Error('Public key or signature not found');
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
'x-public-key': publicKey,
|
|
73
|
+
'x-nonce': nonce,
|
|
74
|
+
'x-signature': signature,
|
|
75
|
+
'x-signature-type': signatureType,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export class TurboArNSPaymentFactory {
|
|
79
|
+
static init(config) {
|
|
80
|
+
const { signer, paymentUrl, axios, logger } = config ?? {};
|
|
81
|
+
if (signer !== undefined) {
|
|
82
|
+
return new TurboArNSPaymentProviderAuthenticated({
|
|
83
|
+
signer,
|
|
84
|
+
paymentUrl,
|
|
85
|
+
axios,
|
|
86
|
+
logger,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return new TurboArNSPaymentProviderUnauthenticated({
|
|
90
|
+
paymentUrl,
|
|
91
|
+
axios,
|
|
92
|
+
logger,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Base class for unauthenticated operations
|
|
97
|
+
export class TurboArNSPaymentProviderUnauthenticated {
|
|
98
|
+
paymentUrl;
|
|
99
|
+
axios;
|
|
100
|
+
logger;
|
|
101
|
+
constructor({ paymentUrl = 'https://payment.ardrive.io', axios = createAxiosInstance(), logger = Logger.default, }) {
|
|
102
|
+
this.paymentUrl = paymentUrl;
|
|
103
|
+
this.axios = axios;
|
|
104
|
+
this.logger = logger;
|
|
105
|
+
}
|
|
106
|
+
async getArNSPriceDetails({ intent, name, quantity, type, years, }) {
|
|
107
|
+
const url = urlWithSearchParams({
|
|
108
|
+
baseUrl: `${this.paymentUrl}/v1/arns/price/${intent}/${name}`,
|
|
109
|
+
params: {
|
|
110
|
+
increaseQty: quantity,
|
|
111
|
+
type,
|
|
112
|
+
years,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const { data, status } = await this.axios.get(url);
|
|
116
|
+
this.logger.debug('getArNSPriceDetails', {
|
|
117
|
+
intent,
|
|
118
|
+
name,
|
|
119
|
+
quantity,
|
|
120
|
+
type,
|
|
121
|
+
years,
|
|
122
|
+
data,
|
|
123
|
+
status,
|
|
124
|
+
});
|
|
125
|
+
if (status !== 200) {
|
|
126
|
+
throw new Error('Failed to get ArNS purchase price ' + JSON.stringify(data));
|
|
127
|
+
}
|
|
128
|
+
if (!data.winc || !data.mARIO) {
|
|
129
|
+
throw new Error('Invalid response from Turbo ' + JSON.stringify(data));
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
winc: data.winc,
|
|
133
|
+
mARIO: new mARIOToken(+data.mARIO),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async getPrice(params) {
|
|
137
|
+
const { winc } = await this.getArNSPriceDetails(params);
|
|
138
|
+
return +winc;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Class for authenticated operations, extending the base class
|
|
142
|
+
export class TurboArNSPaymentProviderAuthenticated extends TurboArNSPaymentProviderUnauthenticated {
|
|
143
|
+
signer;
|
|
144
|
+
constructor({ signer, ...restConfig }) {
|
|
145
|
+
super(restConfig); // Pass unauthenticated config to base class+
|
|
146
|
+
if (!isTurboArNSSigner(signer)) {
|
|
147
|
+
throw new Error('Signer must be a TurboArNSSigner');
|
|
148
|
+
}
|
|
149
|
+
this.signer = signer;
|
|
150
|
+
}
|
|
151
|
+
async initiateArNSPurchase({ intent, name, quantity, type, processId, years, }) {
|
|
152
|
+
// Signer check is implicitly handled by requiring it in the constructor
|
|
153
|
+
const url = urlWithSearchParams({
|
|
154
|
+
baseUrl: `${this.paymentUrl}/v1/arns/purchase/${intent}/${name}`,
|
|
155
|
+
params: {
|
|
156
|
+
increaseQty: quantity,
|
|
157
|
+
processId,
|
|
158
|
+
type,
|
|
159
|
+
years,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const headers = await signedRequestHeadersFromSigner({
|
|
163
|
+
signer: this.signer,
|
|
164
|
+
});
|
|
165
|
+
const { data, status } = await this.axios.post(url, null, {
|
|
166
|
+
headers,
|
|
167
|
+
});
|
|
168
|
+
this.logger.debug('Initiated ArNS purchase', {
|
|
169
|
+
intent,
|
|
170
|
+
name,
|
|
171
|
+
quantity,
|
|
172
|
+
processId,
|
|
173
|
+
type,
|
|
174
|
+
years,
|
|
175
|
+
data,
|
|
176
|
+
status,
|
|
177
|
+
});
|
|
178
|
+
if (status !== 200) {
|
|
179
|
+
throw new Error('Failed to initiate ArNS purchase ' + JSON.stringify(data));
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
id: data.arioWriteResult.id,
|
|
183
|
+
result: data.purchaseReceipt,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function isWanderArweaveBrowserSigner(signer) {
|
|
188
|
+
return (typeof signer === 'object' &&
|
|
189
|
+
signer !== null &&
|
|
190
|
+
'signMessage' in signer &&
|
|
191
|
+
'getActivePublicKey' in signer);
|
|
192
|
+
}
|
|
193
|
+
export function isTurboArNSSigner(signer) {
|
|
194
|
+
const isWanderWallet = isWanderArweaveBrowserSigner(signer);
|
|
195
|
+
const isSigner = signer instanceof EthereumSigner ||
|
|
196
|
+
signer instanceof InjectedEthereumSigner ||
|
|
197
|
+
signer instanceof ArweaveSigner ||
|
|
198
|
+
signer instanceof ArconnectSigner;
|
|
199
|
+
return isWanderWallet || isSigner;
|
|
200
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export class ARIOGatewaysProvider {
|
|
2
|
+
ario;
|
|
3
|
+
constructor({ ario }) {
|
|
4
|
+
this.ario = ario;
|
|
5
|
+
}
|
|
6
|
+
async getGateways() {
|
|
7
|
+
let cursor;
|
|
8
|
+
let attempts = 0;
|
|
9
|
+
const gateways = [];
|
|
10
|
+
do {
|
|
11
|
+
try {
|
|
12
|
+
const { items: newGateways, nextCursor } = await this.ario.getGateways({
|
|
13
|
+
limit: 1000,
|
|
14
|
+
cursor,
|
|
15
|
+
});
|
|
16
|
+
gateways.push(...newGateways);
|
|
17
|
+
cursor = nextCursor;
|
|
18
|
+
attempts = 0; // reset attempts if we get a new cursor
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('Error fetching gateways', {
|
|
22
|
+
cursor,
|
|
23
|
+
attempts,
|
|
24
|
+
error,
|
|
25
|
+
});
|
|
26
|
+
attempts++;
|
|
27
|
+
}
|
|
28
|
+
} while (cursor !== undefined && attempts < 3);
|
|
29
|
+
// filter out any gateways that are not joined
|
|
30
|
+
return gateways.filter((g) => g.status === 'joined');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class StaticGatewaysProvider {
|
|
34
|
+
gateways;
|
|
35
|
+
constructor({ gateways }) {
|
|
36
|
+
this.gateways = gateways;
|
|
37
|
+
}
|
|
38
|
+
async getGateways() {
|
|
39
|
+
return this.gateways;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class SimpleCacheGatewaysProvider {
|
|
43
|
+
gatewaysProvider;
|
|
44
|
+
ttlSeconds;
|
|
45
|
+
lastUpdated;
|
|
46
|
+
gatewaysCache;
|
|
47
|
+
constructor({ gatewaysProvider, ttlSeconds = 5 * 60, // 5 minutes
|
|
48
|
+
}) {
|
|
49
|
+
this.gatewaysCache = [];
|
|
50
|
+
this.gatewaysProvider = gatewaysProvider;
|
|
51
|
+
this.ttlSeconds = ttlSeconds;
|
|
52
|
+
}
|
|
53
|
+
async getGateways() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
if (this.gatewaysCache.length === 0 ||
|
|
56
|
+
now - this.lastUpdated > this.ttlSeconds * 1000) {
|
|
57
|
+
try {
|
|
58
|
+
// preserve the cache if the fetch fails
|
|
59
|
+
const allGateways = await this.gatewaysProvider.getGateways();
|
|
60
|
+
this.gatewaysCache = allGateways.filter((g) => g.status === 'joined');
|
|
61
|
+
this.lastUpdated = now;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('Error fetching gateways', error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return this.gatewaysCache;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
export * from './wayfinder.js';
|
|
17
|
+
// routers
|
|
18
|
+
export * from './routers/random.js';
|
|
19
|
+
export * from './routers/priority.js';
|
|
20
|
+
export * from './routers/fixed.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { FixedGatewayRouter } from './fixed.js';
|
|
4
|
+
describe('FixedRouter', () => {
|
|
5
|
+
it('should return the provided gateway', async () => {
|
|
6
|
+
const router = new FixedGatewayRouter({
|
|
7
|
+
gateway: new URL('http://test-gateway.net'),
|
|
8
|
+
});
|
|
9
|
+
const result = await router.getTargetGateway();
|
|
10
|
+
assert.deepStrictEqual(result, new URL('http://test-gateway.net'));
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { randomInt } from '../../../utils/random.js';
|
|
2
|
+
// TODO: one of N where N are in the last time window have met certain performance thresholds
|
|
3
|
+
// TODO: look at bitorrent routing protocols for inspiration
|
|
4
|
+
// TODO: router that looks at local stats/metrics and adjusts based on those
|
|
5
|
+
export class PriorityGatewayRouter {
|
|
6
|
+
name = 'priority';
|
|
7
|
+
gatewaysProvider;
|
|
8
|
+
limit;
|
|
9
|
+
sortBy;
|
|
10
|
+
sortOrder;
|
|
11
|
+
blocklist;
|
|
12
|
+
constructor({ gatewaysProvider, limit = 1, sortBy = 'operatorStake', sortOrder = 'desc', blocklist = [], }) {
|
|
13
|
+
this.gatewaysProvider = gatewaysProvider;
|
|
14
|
+
this.limit = limit;
|
|
15
|
+
this.sortBy = sortBy;
|
|
16
|
+
this.sortOrder = sortOrder;
|
|
17
|
+
this.blocklist = blocklist;
|
|
18
|
+
}
|
|
19
|
+
async getTargetGateway() {
|
|
20
|
+
const allGateways = await this.gatewaysProvider.getGateways();
|
|
21
|
+
const gateways = allGateways.filter((gateway) => gateway.status === 'joined' &&
|
|
22
|
+
!this.blocklist.includes(gateway.settings.fqdn));
|
|
23
|
+
const sortedGateways = gateways
|
|
24
|
+
.sort(this.sortOrder === 'asc'
|
|
25
|
+
? (a, b) => a[this.sortBy] - b[this.sortBy]
|
|
26
|
+
: (a, b) => b[this.sortBy] - a[this.sortBy])
|
|
27
|
+
.slice(0, this.limit);
|
|
28
|
+
const targetGateway = sortedGateways[randomInt(0, sortedGateways.length)];
|
|
29
|
+
if (targetGateway === undefined) {
|
|
30
|
+
throw new Error('No target gateway found');
|
|
31
|
+
}
|
|
32
|
+
return new URL(`${targetGateway.settings.protocol}://${targetGateway.settings.fqdn}:${targetGateway.settings.port}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { PriorityGatewayRouter } from './priority.js';
|
|
4
|
+
describe('PriorityRouter', () => {
|
|
5
|
+
const mockGateways = [
|
|
6
|
+
{
|
|
7
|
+
settings: {
|
|
8
|
+
fqdn: 'gateway1.net',
|
|
9
|
+
port: 443,
|
|
10
|
+
protocol: 'https',
|
|
11
|
+
allowDelegatedStaking: false,
|
|
12
|
+
delegateRewardShareRatio: 0.5,
|
|
13
|
+
allowedDelegates: [],
|
|
14
|
+
minDelegatedStake: 0,
|
|
15
|
+
autoStake: false,
|
|
16
|
+
properties: '',
|
|
17
|
+
label: '',
|
|
18
|
+
note: '',
|
|
19
|
+
},
|
|
20
|
+
gatewayAddress: 'addr1',
|
|
21
|
+
observerAddress: 'addr1',
|
|
22
|
+
totalDelegatedStake: 1000,
|
|
23
|
+
startTimestamp: 0,
|
|
24
|
+
endTimestamp: 0,
|
|
25
|
+
operatorStake: 100,
|
|
26
|
+
status: 'joined',
|
|
27
|
+
weights: {
|
|
28
|
+
normalizedCompositeWeight: 0.5,
|
|
29
|
+
stakeWeight: 0.5,
|
|
30
|
+
tenureWeight: 0.5,
|
|
31
|
+
gatewayPerformanceRatio: 0.5,
|
|
32
|
+
observerPerformanceRatio: 0.5,
|
|
33
|
+
compositeWeight: 0.5,
|
|
34
|
+
gatewayRewardRatioWeight: 0.5,
|
|
35
|
+
observerRewardRatioWeight: 0.5,
|
|
36
|
+
},
|
|
37
|
+
stats: {
|
|
38
|
+
passedConsecutiveEpochs: 10,
|
|
39
|
+
failedConsecutiveEpochs: 5,
|
|
40
|
+
totalEpochCount: 15,
|
|
41
|
+
passedEpochCount: 10,
|
|
42
|
+
failedEpochCount: 5,
|
|
43
|
+
observedEpochCount: 15,
|
|
44
|
+
prescribedEpochCount: 20,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
settings: {
|
|
49
|
+
fqdn: 'gateway1.net',
|
|
50
|
+
port: 443,
|
|
51
|
+
protocol: 'https',
|
|
52
|
+
allowDelegatedStaking: false,
|
|
53
|
+
delegateRewardShareRatio: 0.5,
|
|
54
|
+
allowedDelegates: [],
|
|
55
|
+
minDelegatedStake: 0,
|
|
56
|
+
autoStake: false,
|
|
57
|
+
properties: '',
|
|
58
|
+
label: '',
|
|
59
|
+
note: '',
|
|
60
|
+
},
|
|
61
|
+
gatewayAddress: 'addr1',
|
|
62
|
+
observerAddress: 'addr1',
|
|
63
|
+
totalDelegatedStake: 2000,
|
|
64
|
+
startTimestamp: 0,
|
|
65
|
+
endTimestamp: 0,
|
|
66
|
+
operatorStake: 2000,
|
|
67
|
+
status: 'joined',
|
|
68
|
+
weights: {
|
|
69
|
+
normalizedCompositeWeight: 0.5,
|
|
70
|
+
stakeWeight: 0.5,
|
|
71
|
+
tenureWeight: 0.5,
|
|
72
|
+
gatewayPerformanceRatio: 0.5,
|
|
73
|
+
observerPerformanceRatio: 0.5,
|
|
74
|
+
compositeWeight: 0.5,
|
|
75
|
+
gatewayRewardRatioWeight: 0.5,
|
|
76
|
+
observerRewardRatioWeight: 0.5,
|
|
77
|
+
},
|
|
78
|
+
stats: {
|
|
79
|
+
passedConsecutiveEpochs: 10,
|
|
80
|
+
failedConsecutiveEpochs: 5,
|
|
81
|
+
totalEpochCount: 15,
|
|
82
|
+
passedEpochCount: 10,
|
|
83
|
+
failedEpochCount: 5,
|
|
84
|
+
observedEpochCount: 15,
|
|
85
|
+
prescribedEpochCount: 20,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
settings: {
|
|
90
|
+
fqdn: 'gateway2.net',
|
|
91
|
+
port: 443,
|
|
92
|
+
protocol: 'https',
|
|
93
|
+
allowDelegatedStaking: false,
|
|
94
|
+
delegateRewardShareRatio: 0.5,
|
|
95
|
+
allowedDelegates: [],
|
|
96
|
+
minDelegatedStake: 0,
|
|
97
|
+
autoStake: false,
|
|
98
|
+
properties: '',
|
|
99
|
+
label: '',
|
|
100
|
+
note: '',
|
|
101
|
+
},
|
|
102
|
+
gatewayAddress: 'addr2',
|
|
103
|
+
observerAddress: 'addr2',
|
|
104
|
+
totalDelegatedStake: 0,
|
|
105
|
+
startTimestamp: 0,
|
|
106
|
+
endTimestamp: 0,
|
|
107
|
+
operatorStake: 0,
|
|
108
|
+
status: 'leaving',
|
|
109
|
+
weights: {
|
|
110
|
+
normalizedCompositeWeight: 0.5,
|
|
111
|
+
stakeWeight: 0.5,
|
|
112
|
+
tenureWeight: 0.5,
|
|
113
|
+
gatewayPerformanceRatio: 0.5,
|
|
114
|
+
observerPerformanceRatio: 0.5,
|
|
115
|
+
compositeWeight: 0.5,
|
|
116
|
+
gatewayRewardRatioWeight: 0.5,
|
|
117
|
+
observerRewardRatioWeight: 0.5,
|
|
118
|
+
},
|
|
119
|
+
stats: {
|
|
120
|
+
passedConsecutiveEpochs: 10,
|
|
121
|
+
failedConsecutiveEpochs: 5,
|
|
122
|
+
totalEpochCount: 15,
|
|
123
|
+
passedEpochCount: 10,
|
|
124
|
+
failedEpochCount: 5,
|
|
125
|
+
observedEpochCount: 15,
|
|
126
|
+
prescribedEpochCount: 20,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
const mockGatewaysProvider = {
|
|
131
|
+
getGateways: async () => mockGateways,
|
|
132
|
+
};
|
|
133
|
+
it('should prioritize gateway with highest success rate when using successRate weight', async () => {
|
|
134
|
+
const router = new PriorityGatewayRouter({
|
|
135
|
+
gatewaysProvider: mockGatewaysProvider,
|
|
136
|
+
sortBy: 'totalDelegatedStake',
|
|
137
|
+
sortOrder: 'desc',
|
|
138
|
+
limit: 1,
|
|
139
|
+
});
|
|
140
|
+
const result = await router.getTargetGateway();
|
|
141
|
+
assert.deepStrictEqual(result, new URL('http://gateway1.net'));
|
|
142
|
+
});
|
|
143
|
+
it('should prioritize gateway with lowest latency when using latency weight', async () => {
|
|
144
|
+
const router = new PriorityGatewayRouter({
|
|
145
|
+
gatewaysProvider: mockGatewaysProvider,
|
|
146
|
+
sortBy: 'operatorStake',
|
|
147
|
+
sortOrder: 'desc',
|
|
148
|
+
limit: 1,
|
|
149
|
+
});
|
|
150
|
+
const result = await router.getTargetGateway();
|
|
151
|
+
assert.deepStrictEqual(result, new URL('http://gateway2.net'));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomInt } from '../../../utils/random.js';
|
|
2
|
+
export class RandomGatewayRouter {
|
|
3
|
+
name = 'random';
|
|
4
|
+
gatewaysProvider;
|
|
5
|
+
blocklist;
|
|
6
|
+
constructor({ gatewaysProvider, blocklist = [], }) {
|
|
7
|
+
this.gatewaysProvider = gatewaysProvider;
|
|
8
|
+
this.blocklist = blocklist;
|
|
9
|
+
}
|
|
10
|
+
async getTargetGateway() {
|
|
11
|
+
const allGateways = await this.gatewaysProvider.getGateways();
|
|
12
|
+
const gateways = allGateways.filter((g) => g.status === 'joined' && !this.blocklist.includes(g.settings.fqdn));
|
|
13
|
+
const targetGateway = gateways[randomInt(0, gateways.length)];
|
|
14
|
+
if (targetGateway === undefined) {
|
|
15
|
+
throw new Error('No target gateway found');
|
|
16
|
+
}
|
|
17
|
+
return new URL(`${targetGateway.settings.protocol}://${targetGateway.settings.fqdn}:${targetGateway.settings.port}`);
|
|
18
|
+
}
|
|
19
|
+
}
|