@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.
Files changed (96) hide show
  1. package/README.md +66 -125
  2. package/bundles/web.bundle.min.js +130 -125
  3. package/lib/cjs/cli/options.js +12 -0
  4. package/lib/cjs/cli/utils.js +32 -10
  5. package/lib/cjs/common/ant-versions.js +5 -5
  6. package/lib/cjs/common/index.js +2 -0
  7. package/lib/cjs/common/io.js +64 -1
  8. package/lib/cjs/common/turbo.js +208 -0
  9. package/lib/cjs/common/wayfinder/gateways.js +75 -0
  10. package/lib/cjs/common/wayfinder/index.js +36 -0
  11. package/lib/cjs/common/wayfinder/routers/fixed.js +14 -0
  12. package/lib/cjs/common/wayfinder/routers/fixed.test.js +14 -0
  13. package/lib/cjs/common/wayfinder/routers/priority.js +38 -0
  14. package/lib/cjs/common/wayfinder/routers/priority.test.js +155 -0
  15. package/lib/cjs/common/wayfinder/routers/random.js +23 -0
  16. package/lib/cjs/common/wayfinder/routers/random.test.js +104 -0
  17. package/lib/cjs/common/wayfinder/routers/simple-cache.js +25 -0
  18. package/lib/cjs/common/wayfinder/routers/simple-cache.test.js +41 -0
  19. package/lib/cjs/common/wayfinder/wayfinder.js +167 -0
  20. package/lib/cjs/common/wayfinder/wayfinder.test.js +253 -0
  21. package/lib/cjs/types/ant.js +12 -1
  22. package/lib/cjs/types/index.js +1 -0
  23. package/lib/cjs/types/io.js +1 -1
  24. package/lib/cjs/types/wayfinder.js +2 -0
  25. package/lib/cjs/utils/ao.js +5 -0
  26. package/lib/cjs/utils/random.js +30 -0
  27. package/lib/cjs/utils/url.js +28 -0
  28. package/lib/cjs/utils/url.test.js +24 -0
  29. package/lib/cjs/utils/utils.test.js +8 -8
  30. package/lib/cjs/version.js +1 -1
  31. package/lib/cjs/web/index.js +3 -1
  32. package/lib/esm/cli/options.js +12 -0
  33. package/lib/esm/cli/utils.js +31 -10
  34. package/lib/esm/common/ant-versions.js +5 -5
  35. package/lib/esm/common/index.js +2 -0
  36. package/lib/esm/common/io.js +64 -1
  37. package/lib/esm/common/turbo.js +200 -0
  38. package/lib/esm/common/wayfinder/gateways.js +69 -0
  39. package/lib/esm/common/wayfinder/index.js +20 -0
  40. package/lib/esm/common/wayfinder/routers/fixed.js +10 -0
  41. package/lib/esm/common/wayfinder/routers/fixed.test.js +12 -0
  42. package/lib/esm/common/wayfinder/routers/priority.js +34 -0
  43. package/lib/esm/common/wayfinder/routers/priority.test.js +153 -0
  44. package/lib/esm/common/wayfinder/routers/random.js +19 -0
  45. package/lib/esm/common/wayfinder/routers/random.test.js +102 -0
  46. package/lib/esm/common/wayfinder/routers/simple-cache.js +21 -0
  47. package/lib/esm/common/wayfinder/routers/simple-cache.test.js +39 -0
  48. package/lib/esm/common/wayfinder/wayfinder.js +161 -0
  49. package/lib/esm/common/wayfinder/wayfinder.test.js +248 -0
  50. package/lib/esm/types/ant.js +11 -0
  51. package/lib/esm/types/index.js +1 -0
  52. package/lib/esm/types/io.js +1 -1
  53. package/lib/esm/types/wayfinder.js +1 -0
  54. package/lib/esm/utils/ao.js +5 -0
  55. package/lib/esm/utils/random.js +26 -0
  56. package/lib/esm/utils/url.js +24 -0
  57. package/lib/esm/utils/url.test.js +19 -0
  58. package/lib/esm/utils/utils.test.js +1 -1
  59. package/lib/esm/version.js +1 -1
  60. package/lib/esm/web/index.js +1 -1
  61. package/lib/types/cli/commands/antCommands.d.ts +3 -3
  62. package/lib/types/cli/commands/arnsPurchaseCommands.d.ts +1 -1
  63. package/lib/types/cli/commands/gatewayWriteCommands.d.ts +9 -9
  64. package/lib/types/cli/commands/readCommands.d.ts +1 -0
  65. package/lib/types/cli/commands/transfer.d.ts +3 -3
  66. package/lib/types/cli/options.d.ts +9 -0
  67. package/lib/types/cli/types.d.ts +3 -0
  68. package/lib/types/cli/utils.d.ts +4 -0
  69. package/lib/types/common/ant-versions.d.ts +3 -6
  70. package/lib/types/common/index.d.ts +1 -0
  71. package/lib/types/common/io.d.ts +9 -7
  72. package/lib/types/common/turbo.d.ts +62 -0
  73. package/lib/types/common/wayfinder/gateways.d.ts +44 -0
  74. package/lib/types/common/wayfinder/index.d.ts +19 -0
  75. package/lib/types/common/wayfinder/routers/fixed.d.ts +24 -0
  76. package/lib/types/common/wayfinder/routers/fixed.test.d.ts +1 -0
  77. package/lib/types/common/wayfinder/routers/priority.d.ts +33 -0
  78. package/lib/types/common/wayfinder/routers/priority.test.d.ts +1 -0
  79. package/lib/types/common/wayfinder/routers/random.d.ts +27 -0
  80. package/lib/types/common/wayfinder/routers/random.test.d.ts +1 -0
  81. package/lib/types/common/wayfinder/routers/simple-cache.d.ts +28 -0
  82. package/lib/types/common/wayfinder/routers/simple-cache.test.d.ts +1 -0
  83. package/lib/types/common/wayfinder/wayfinder.d.ts +113 -0
  84. package/lib/types/common/wayfinder/wayfinder.test.d.ts +1 -0
  85. package/lib/types/types/ant.d.ts +53 -1
  86. package/lib/types/types/common.d.ts +7 -2
  87. package/lib/types/types/index.d.ts +1 -0
  88. package/lib/types/types/io.d.ts +7 -5
  89. package/lib/types/types/wayfinder.d.ts +19 -0
  90. package/lib/types/utils/ao.d.ts +1 -12
  91. package/lib/types/utils/random.d.ts +7 -0
  92. package/lib/types/utils/url.d.ts +19 -0
  93. package/lib/types/utils/url.test.d.ts +1 -0
  94. package/lib/types/version.d.ts +1 -1
  95. package/lib/types/web/index.d.ts +1 -1
  96. package/package.json +5 -3
@@ -22,3 +22,5 @@ export * from './faucet.js';
22
22
  // ao
23
23
  export * from './io.js';
24
24
  export * from './contracts/ao-process.js';
25
+ // wayfinder
26
+ export * from './wayfinder/index.js';
@@ -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
- constructor({ signer, ...config }) {
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,10 @@
1
+ export class FixedGatewayRouter {
2
+ name = 'fixed';
3
+ gateway;
4
+ constructor({ gateway }) {
5
+ this.gateway = gateway;
6
+ }
7
+ async getTargetGateway() {
8
+ return this.gateway;
9
+ }
10
+ }
@@ -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
+ }