diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index af94b0cd7..708e02acb 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest env: GITHUB_URL: https://github.com/${{ github.repository }} - ADD_CHROME_VERSION: 120 + ADD_CHROME_VERSION: 127 IMAGE: ulixee/ulixee-cloud steps: diff --git a/client/main/package.json b/client/main/package.json index 698e7110e..f4d2eaa20 100644 --- a/client/main/package.json +++ b/client/main/package.json @@ -8,7 +8,7 @@ "dependencies": { "@ulixee/commons": "2.0.0-alpha.29", "@ulixee/datastore": "2.0.0-alpha.29", - "@argonprotocol/localchain": "0.0.6", + "@argonprotocol/localchain": "0.0.8", "@ulixee/net": "2.0.0-alpha.29", "@ulixee/schema": "2.0.0-alpha.29" }, diff --git a/cloud/main/cli/index.ts b/cloud/main/cli/index.ts index 959eb94ef..d30827cab 100644 --- a/cloud/main/cli/index.ts +++ b/cloud/main/cli/index.ts @@ -1,6 +1,7 @@ import { applyEnvironmentVariables, parseEnvBool } from '@ulixee/commons/lib/envUtils'; import { filterUndefined } from '@ulixee/commons/lib/objectUtils'; -import { parseIdentities } from '@ulixee/datastore-core/env'; +import { parseAddress, parseIdentities } from '@ulixee/datastore-core/env'; +import type ILocalchainConfig from '@ulixee/datastore/interfaces/ILocalchainConfig'; import { Command } from 'commander'; import * as Path from 'path'; import CloudNodeEnv from '../env'; @@ -88,15 +89,16 @@ export default function cliCommands(): Command { ) .argParser(parseInt) .default(10), - ).addOption( - program - .createOption( - '--max-concurrent-heroes-per-browser ', - 'Max number of concurrent Heroes to run per Chrome instance.', - ) - .argParser(parseInt) - .default(10), - ) + ) + .addOption( + program + .createOption( + '--max-concurrent-heroes-per-browser ', + 'Max number of concurrent Heroes to run per Chrome instance.', + ) + .argParser(parseInt) + .default(10), + ) .addOption( program .createOption( @@ -139,8 +141,77 @@ export default function cliCommands(): Command { '--datastore-wait-for-completion', 'Wait for all in-process Datastores to complete before shutting down the Cloud node.', ) + .argParser(parseEnvBool) .default(false), ) + .addOption( + program + .createOption( + '--argon-payment-address
', + 'The SS58 formatted address to send payments to.', + ) + .argParser(x => parseAddress(x, 'Argon Payments Address')) + .env('ARGON_PAYMENT_ADDRESS'), + ) + .addOption( + program + .createOption( + '--argon-notary-id ', + 'The preferred Argon notary to notarize payments with.', + ) + .argParser(parseInt) + .env('ARGON_NOTARY_ID'), + ) + .addOption( + program + .createOption( + '--argon-localchain-path ', + 'Activate payment receipt with the given Argon Localchain.', + ) + .env('ARGON_LOCALCHAIN_PATH'), + ) + .addOption( + program + .createOption( + '--argon-mainchain-url ', + 'Connect to the given Argon Mainchain rpc url.', + ) + .env('ARGON_MAINCHAIN_URL'), + ) + .addOption( + program + .createOption( + '--argon-localchain-password ', + 'Localchain password provided inline (unsafe).', + ) + .env('ARGON_LOCALCHAIN_PASSWORD'), + ) + .addOption( + program + .createOption( + '--argon-localchain-password-interactive', + 'Localchain password prompted on command line.', + ) + .argParser(parseEnvBool) + .env('ARGON_LOCALCHAIN_PASSWORD_INTERACTIVE_CLI'), + ) + .addOption( + program + .createOption( + '--argon-localchain-password-file ', + 'Localchain password from a file path.', + ) + .env('ARGON_LOCALCHAIN_PASSWORD_FILE'), + ) + .addOption( + program + .createOption( + '--argon-block-rewards-address
', + 'Activate block rewards capture with the given Argon Localchain address.', + ) + .argParser(x => parseAddress(x, 'Block Rewards Address')) + .env('ARGON_BLOCK_REWARDS_ADDRESS'), + ) .allowUnknownOption(true) .action(async opts => { console.log('Starting Ulixee Cloud with configuration:', opts); @@ -152,13 +223,36 @@ export default function cliCommands(): Command { hostedServicesPort, hostedServicesHostname, env, + argonPaymentAddress, + argonNotaryId, + argonLocalchainPath, + argonMainchainUrl, + argonLocalchainPassword, + argonLocalchainPasswordInteractive, + argonLocalchainPasswordFile, + argonBlockRewardsAddress, } = opts; if (env) { applyEnvironmentVariables(Path.resolve(env), process.env); } if (disableChromeAlive) CloudNodeEnv.disableChromeAlive = disableChromeAlive; - const { unblockedPlugins, heroDataDir, maxConcurrentHeroes, maxConcurrentHeroesPerBrowser } = opts; + const { unblockedPlugins, heroDataDir, maxConcurrentHeroes, maxConcurrentHeroesPerBrowser } = + opts; + let localchainConfig: ILocalchainConfig; + if (argonLocalchainPath) { + localchainConfig = { + localchainPath: argonLocalchainPath, + mainchainUrl: argonMainchainUrl, + notaryId: argonNotaryId, + keystorePassword: { + password: argonLocalchainPassword ? Buffer.from(argonLocalchainPassword) : undefined, + interactiveCli: argonLocalchainPasswordInteractive, + passwordFile: argonLocalchainPasswordFile, + }, + blockRewardsAddress: argonBlockRewardsAddress, + }; + } const cloudNode = new CloudNode( filterUndefined({ @@ -186,6 +280,14 @@ export default function cliCommands(): Command { maxRuntimeMs: opts.maxDatastoreRuntimeMs, waitForDatastoreCompletionOnShutdown: opts.datastoreWaitForCompletion, adminIdentities: parseIdentities(opts.adminIdentities, 'Admin Identities'), + paymentInfo: + argonPaymentAddress && argonNotaryId + ? { + address: argonPaymentAddress, + notaryId: argonNotaryId, + } + : undefined, + localchainConfig, }), }), ); diff --git a/datastore/broker/env.ts b/datastore/broker/env.ts index 4e6720d57..b8c1696a2 100644 --- a/datastore/broker/env.ts +++ b/datastore/broker/env.ts @@ -27,7 +27,7 @@ function getLocalchainConfig(): ILocalchainConfig | undefined { return { localchainPath: env.ARGON_LOCALCHAIN_PATH, mainchainUrl: env.ARGON_MAINCHAIN_URL, - notaryId: parseEnvInt(env.NOTARY_ID), + notaryId: parseEnvInt(env.ARGON_NOTARY_ID), keystorePassword: { interactiveCli: parseEnvBool(env.ARGON_LOCALCHAIN_PASSWORD_INTERACTIVE_CLI), password: keystorePassword, diff --git a/datastore/broker/package.json b/datastore/broker/package.json index 7880d5dbe..1ab278cec 100644 --- a/datastore/broker/package.json +++ b/datastore/broker/package.json @@ -20,7 +20,7 @@ "@ulixee/cloud": "2.0.0-alpha.29", "@ulixee/commons": "2.0.0-alpha.29", "@ulixee/datastore": "2.0.0-alpha.29", - "@argonprotocol/localchain": "0.0.6", + "@argonprotocol/localchain": "0.0.8", "@ulixee/net": "2.0.0-alpha.29", "@ulixee/platform-specification": "2.0.0-alpha.29", "@ulixee/platform-utils": "2.0.0-alpha.29", diff --git a/datastore/broker/test/api.test.ts b/datastore/broker/test/api.test.ts index 22e8770b1..d14d41ce8 100644 --- a/datastore/broker/test/api.test.ts +++ b/datastore/broker/test/api.test.ts @@ -4,7 +4,7 @@ import { toUrl } from '@ulixee/commons/lib/utils'; import { Helpers } from '@ulixee/datastore-testing'; import BrokerChannelHoldSource from '@ulixee/datastore/payments/BrokerChannelHoldSource'; import LocalchainWithSync from '@ulixee/datastore/payments/LocalchainWithSync'; -import { ADDRESS_PREFIX } from '@argonprotocol/localchain'; +import { ADDRESS_PREFIX, Chain } from '@argonprotocol/localchain'; import { ConnectionToCore, WsTransportToCore } from '@ulixee/net'; import { IDatabrokerAdminApis } from '@ulixee/platform-specification/datastore'; import Identity from '@ulixee/platform-utils/lib/Identity'; @@ -62,6 +62,11 @@ beforeEach(() => { afterEach(Helpers.afterEach); afterAll(Helpers.afterAll); +const mainchainIdentity = { + chain: Chain.Devnet, + genesisHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', +}; + it('can create channelHolds', async () => { const broker = new DataBroker({ storageDir }); Helpers.needsClosing.push(broker); @@ -85,6 +90,7 @@ it('can create channelHolds', async () => { recipient: { address: datastoreKeyPair.address, notaryId: 1, + ...mainchainIdentity, }, id: 'test', version: '1.0.0', @@ -104,10 +110,12 @@ it('can create channelHolds', async () => { await broker.onLocalchainSync({ channelHoldNotarizations: [ { - channelHolds: [{ - id: channelHold.channelHoldId, - settledAmount: 80n, - }], + channelHolds: [ + { + id: channelHold.channelHoldId, + settledAmount: 80n, + }, + ], }, ], } as any); @@ -137,6 +145,7 @@ test('it rejects invalid signing requests', async () => { recipient: { address: datastoreKeyPair.address, notaryId: 1, + ...mainchainIdentity, }, id: 'test', version: '1.0.0', @@ -144,7 +153,9 @@ test('it rejects invalid signing requests', async () => { }; const client = new BrokerChannelHoldSource(brokerHost.href, await Identity.create()); - await expect(client.createChannelHold(paymentInfo, 100n)).rejects.toThrow('Organization not found'); + await expect(client.createChannelHold(paymentInfo, 100n)).rejects.toThrow( + 'Organization not found', + ); jest .spyOn(BrokerChannelHoldSource, 'createSignatureMessage') diff --git a/datastore/core/.env.defaults b/datastore/core/.env.defaults index d4b41e0be..3fce8f9c4 100644 --- a/datastore/core/.env.defaults +++ b/datastore/core/.env.defaults @@ -28,6 +28,8 @@ ULX_ENABLE_GLOBAL_CONFIG=true ## Localchain Configuration +# Alternative configuration to a machine-local localchain +ARGON_PAYMENT_ADDRESS= # Path to the localchain ARGON_LOCALCHAIN_PATH= # Default Argon mainchain host TODO: fill in once mainchain is up# diff --git a/datastore/core/endpoints/ChannelHold.register.ts b/datastore/core/endpoints/ChannelHold.register.ts index e0c4a9497..728798446 100644 --- a/datastore/core/endpoints/ChannelHold.register.ts +++ b/datastore/core/endpoints/ChannelHold.register.ts @@ -9,7 +9,7 @@ export default new ValidatingApiHandler('ChannelHold.register', ChannelHoldApisS request, context: IDatastoreApiContext, ): Promise { - const manifest = await context.datastoreRegistry.get(request.datastoreId); + const manifest = await context.datastoreRegistry.get(request.datastoreId, null, false); if (!manifest) throw new Error(`Unknown datastore requested ${request.datastoreId}`); await context.micropaymentChannelSpendTracker.importChannelHold(request, manifest); return { accepted: true }; diff --git a/datastore/core/endpoints/Datastore.meta.ts b/datastore/core/endpoints/Datastore.meta.ts index 285721942..b3d380adb 100644 --- a/datastore/core/endpoints/Datastore.meta.ts +++ b/datastore/core/endpoints/Datastore.meta.ts @@ -5,7 +5,8 @@ export default new DatastoreApiHandler('Datastore.meta', { async handler(request, context) { const datastore = await context.datastoreRegistry.get(request.id, request.version); const stats = await context.statsTracker.getForDatastoreVersion(datastore); + const paymentInfo = await context.paymentInfo; - return translateDatastoreMetadata(datastore, stats, request.includeSchemasAsJson); + return translateDatastoreMetadata(datastore, stats, request.includeSchemasAsJson, paymentInfo); }, }); diff --git a/datastore/core/endpoints/HostedServicesEndpoints.ts b/datastore/core/endpoints/HostedServicesEndpoints.ts index 32dbef974..a087b666b 100644 --- a/datastore/core/endpoints/HostedServicesEndpoints.ts +++ b/datastore/core/endpoints/HostedServicesEndpoints.ts @@ -88,6 +88,9 @@ export default class HostedServicesEndpoints { const manifest = await ctx.datastoreRegistry.get(datastoreId, version); return await ctx.statsTracker.getForDatastoreVersion(manifest); }, + 'MicropaymentChannel.getPaymentInfo': async (_args, ctx) => { + return await ctx.micropaymentChannelSpendTracker.getPaymentInfo(); + }, 'MicropaymentChannel.importChannelHold': async ({ channelHold, datastoreId }, ctx) => { const manifest = await ctx.datastoreRegistry.get(datastoreId); return await ctx.micropaymentChannelSpendTracker.importChannelHold({ channelHold, datastoreId }, manifest); diff --git a/datastore/core/env.ts b/datastore/core/env.ts index 54ca788e8..1c321ea41 100644 --- a/datastore/core/env.ts +++ b/datastore/core/env.ts @@ -1,8 +1,8 @@ import { loadEnv, parseEnvBool, parseEnvInt, parseEnvPath } from '@ulixee/commons/lib/envUtils'; +import ILocalchainConfig from '@ulixee/datastore/interfaces/ILocalchainConfig'; import { addressValidation, identityValidation } from '@ulixee/platform-specification/types'; import * as Os from 'os'; import * as Path from 'path'; -import ILocalchainConfig from '@ulixee/datastore/interfaces/ILocalchainConfig'; loadEnv(process.cwd()); loadEnv(__dirname); @@ -27,6 +27,7 @@ export default { datastoresMustHaveOwnAdminIdentity: parseEnvBool(env.ULX_DATASTORES_MUST_HAVE_OWN_ADMIN) ?? false, localchainConfig: getLocalchainConfig(), + argonMainchainUrl: env.ARGON_MAINCHAIN_URL, enableGlobalConfigs: parseEnvBool(env.ULX_ENABLE_GLOBAL_CONFIG) ?? true, statsTrackerHost: env.ULX_DATASTORE_STATS_HOST, @@ -43,13 +44,14 @@ function getLocalchainConfig(): ILocalchainConfig | undefined { } if (env.ARGON_LOCALCHAIN_PASSWORD_FILE) env.ARGON_LOCALCHAIN_PASSWORD_FILE = parseEnvPath(env.ARGON_LOCALCHAIN_PASSWORD_FILE); - if (env.ARGON_LOCALCHAIN_PATH) env.ARGON_LOCALCHAIN_PATH = parseEnvPath(env.ARGON_LOCALCHAIN_PATH); + if (env.ARGON_LOCALCHAIN_PATH) + env.ARGON_LOCALCHAIN_PATH = parseEnvPath(env.ARGON_LOCALCHAIN_PATH); return { localchainPath: env.ARGON_LOCALCHAIN_PATH, mainchainUrl: env.ARGON_MAINCHAIN_URL, blockRewardsAddress: parseAddress(env.ARGON_BLOCK_REWARDS_ADDRESS, 'Block Rewards Address'), - notaryId: parseEnvInt(env.NOTARY_ID), + notaryId: parseEnvInt(env.ARGON_NOTARY_ID), keystorePassword: { interactiveCli: parseEnvBool(env.ARGON_LOCALCHAIN_PASSWORD_INTERACTIVE_CLI), password: keystorePassword, @@ -58,7 +60,7 @@ function getLocalchainConfig(): ILocalchainConfig | undefined { }; } -function parseAddress(address: string, type: string): string { +export function parseAddress(address: string, type: string): string { if (!address) return address; try { addressValidation.parse(address); diff --git a/datastore/core/index.ts b/datastore/core/index.ts index 75220bbc9..912b1832b 100644 --- a/datastore/core/index.ts +++ b/datastore/core/index.ts @@ -25,7 +25,8 @@ import { promises as Fs } from 'fs'; import { IncomingMessage, ServerResponse } from 'http'; import * as Path from 'path'; import MicropaymentChannelSpendTracker from '@ulixee/datastore-core/lib/MicropaymentChannelSpendTracker'; -import IMicropaymentChannelSpendTracker from "@ulixee/datastore-core/interfaces/IMicropaymentChannelSpendTracker"; +import IMicropaymentChannelSpendTracker from '@ulixee/datastore-core/interfaces/IMicropaymentChannelSpendTracker'; +import { IDatastorePaymentRecipient } from '@ulixee/platform-specification/types/IDatastoreManifest'; import DatastoreAdmin from './endpoints/Datastore.admin'; import DatastoreCreateStorageEngine from './endpoints/Datastore.createStorageEngine'; import DatastoreCreditsBalance from './endpoints/Datastore.creditsBalance'; @@ -89,6 +90,7 @@ export default class DatastoreCore extends TypedEventEmitter<{ public isClosing: Promise; public workTracker: WorkTracker; + public paymentInfo = new Resolvable(); public apiRegistry = new ApiRegistry([ DatastoreQuery, @@ -305,14 +307,7 @@ export default class DatastoreCore extends TypedEventEmitter<{ if (lookupConnection) this.datastoreHostLookup = new DatastoreHostLookupClient(lookupConnection); - const paymentServiceConnection = createConnectionToServiceHost( - this.options.paymentServiceHost, - ); - - if (paymentServiceConnection) { - const argonReserver = new RemoteReserver(paymentServiceConnection); - this.upstreamDatastorePaymentService = new EmbeddedPaymentService(argonReserver); - } else if (this.options.localchainConfig?.localchainPath) { + if (this.options.localchainConfig?.localchainPath) { this.localchain = new LocalchainWithSync(this.options.localchainConfig); await this.localchain.load(); @@ -324,26 +319,41 @@ export default class DatastoreCore extends TypedEventEmitter<{ this.options.datastoresDir, this.localchain, ); + + this.paymentInfo.resolve(await this.localchain.paymentInfo); } else { - this.upstreamDatastorePaymentService = new EmbeddedPaymentService(); - if (!this.datastoreHostLookup) { - const argonMainchainUrl = - this.options.localchainConfig?.mainchainUrl ?? Env.localchainConfig?.mainchainUrl; - const mainchainClient = argonMainchainUrl - ? await MainchainClient.connect(argonMainchainUrl, 10e3) - : null; - this.datastoreHostLookup = new DatastoreLookup(mainchainClient); - } + const paymentServiceConnection = createConnectionToServiceHost( + this.options.paymentServiceHost, + ); + const argonReserver = paymentServiceConnection + ? new RemoteReserver(paymentServiceConnection) + : undefined; + + this.upstreamDatastorePaymentService = new EmbeddedPaymentService(argonReserver); const channelHoldConnection = createConnectionToServiceHost( this.options.micropaymentChannelSpendTrackingHost, ); if (channelHoldConnection) { - this.micropaymentChannelSpendTracker = new MicropaymentChannelSpendTrackerClient(channelHoldConnection); + this.micropaymentChannelSpendTracker = new MicropaymentChannelSpendTrackerClient( + channelHoldConnection, + ); + this.paymentInfo.resolve(await this.micropaymentChannelSpendTracker.getPaymentInfo()); } } + if (!this.paymentInfo.isResolved) { + log.info( + "DatastoreCore.start - No Argon Payment information found. Can't charge for Datastores.", + ); + this.paymentInfo.resolve(undefined); + } - this.datastoreHostLookup ??= this.upstreamDatastorePaymentService.datastoreLookup; + if (!this.datastoreHostLookup) { + const mainchainClient = Env.argonMainchainUrl + ? await MainchainClient.connect(Env.argonMainchainUrl, 10e3) + : undefined; + this.datastoreHostLookup = new DatastoreLookup(mainchainClient); + } this.vm = new DatastoreVm( this.connectionToThisCore, @@ -509,6 +519,7 @@ export default class DatastoreCore extends TypedEventEmitter<{ workTracker: this.workTracker, configuration: this.options, pluginCoresByName: this.pluginCoresByName, + paymentInfo: this.paymentInfo.promise, storageEngineRegistry: this.storageEngineRegistry, cloudNodeAddress: this.cloudNodeAddress, cloudNodeIdentity: this.cloudNodeIdentity, diff --git a/datastore/core/interfaces/IDatastoreApiContext.ts b/datastore/core/interfaces/IDatastoreApiContext.ts index 8ad7caa56..62aec7a2f 100644 --- a/datastore/core/interfaces/IDatastoreApiContext.ts +++ b/datastore/core/interfaces/IDatastoreApiContext.ts @@ -5,6 +5,7 @@ import IPaymentService from '@ulixee/datastore/interfaces/IPaymentService'; import DatastoreApiClients from '@ulixee/datastore/lib/DatastoreApiClients'; import Identity from '@ulixee/platform-utils/lib/Identity'; import IMicropaymentChannelSpendTracker from '@ulixee/datastore-core/interfaces/IMicropaymentChannelSpendTracker'; +import { IDatastorePaymentRecipient } from '@ulixee/platform-specification/types/IDatastoreManifest'; import DatastoreRegistry from '../lib/DatastoreRegistry'; import DatastoreVm from '../lib/DatastoreVm'; import StatsTracker from '../lib/StatsTracker'; @@ -17,6 +18,7 @@ export default interface IDatastoreApiContext { logger: IBoundLog; datastoreRegistry: DatastoreRegistry; micropaymentChannelSpendTracker: IMicropaymentChannelSpendTracker; + paymentInfo: Promise; upstreamDatastorePaymentService: IPaymentService; datastoreLookup: IDatastoreHostLookup; storageEngineRegistry?: StorageEngineRegistry; diff --git a/datastore/core/interfaces/IMicropaymentChannelSpendTracker.ts b/datastore/core/interfaces/IMicropaymentChannelSpendTracker.ts index 7dcd89ce2..4be8f34b2 100644 --- a/datastore/core/interfaces/IMicropaymentChannelSpendTracker.ts +++ b/datastore/core/interfaces/IMicropaymentChannelSpendTracker.ts @@ -1,7 +1,10 @@ import IMicropaymentChannelApiTypes from '@ulixee/platform-specification/services/MicropaymentChannelApis'; -import IDatastoreManifest from '@ulixee/platform-specification/types/IDatastoreManifest'; +import IDatastoreManifest, { + IDatastorePaymentRecipient, +} from '@ulixee/platform-specification/types/IDatastoreManifest'; export default interface IMicropaymentChannelSpendTracker { + getPaymentInfo(): Promise; debit( data: IMicropaymentChannelApiTypes['MicropaymentChannel.debitPayment']['args'], ): Promise; diff --git a/datastore/core/lib/DatastoreManifest.ts b/datastore/core/lib/DatastoreManifest.ts index 7de524012..9560d6798 100644 --- a/datastore/core/lib/DatastoreManifest.ts +++ b/datastore/core/lib/DatastoreManifest.ts @@ -7,7 +7,6 @@ import IDatastoreMetadata from '@ulixee/datastore/interfaces/IDatastoreMetadata' import { datastoreIdValidation } from '@ulixee/platform-specification/types/datastoreIdValidation'; import IDatastoreManifest, { DatastoreManifestSchema, - IDatastorePaymentRecipient, } from '@ulixee/platform-specification/types/IDatastoreManifest'; import ValidationError from '@ulixee/platform-specification/utils/ValidationError'; import { promises as Fs } from 'fs'; @@ -38,8 +37,6 @@ export default class DatastoreManifest implements IDatastoreManifest { public tablesByName: IDatastoreManifest['tablesByName'] = {}; public adminIdentities: string[]; - // Payment details - public payment?: IDatastorePaymentRecipient; public domain?: string; @@ -214,7 +211,7 @@ export default class DatastoreManifest implements IDatastoreManifest { } else if (this.source === 'dbx') { // dbx stores only the output json = this.toJSON(); - await DatastoreManifest.validate(json); + DatastoreManifest.validate(json); } // don't create file if it doesn't exist already @@ -246,7 +243,6 @@ export default class DatastoreManifest implements IDatastoreManifest { crawlersByName: this.crawlersByName, storageEngineHost: this.storageEngineHost, tablesByName: this.tablesByName, - payment: this.payment, domain: this.domain, adminIdentities: this.adminIdentities, }; diff --git a/datastore/core/lib/DatastoreRegistry.ts b/datastore/core/lib/DatastoreRegistry.ts index 574dbc83f..5571ffb00 100644 --- a/datastore/core/lib/DatastoreRegistry.ts +++ b/datastore/core/lib/DatastoreRegistry.ts @@ -127,7 +127,7 @@ export default class DatastoreRegistry extends TypedEventEmitter<{ if (manifestWithLatest) return manifestWithLatest; if (throwIfNotExists) { - throw new DatastoreNotFoundError(`Datastore version (${version}) not found on Cloud.`, { + throw new DatastoreNotFoundError(`Datastore version (${id}/v${version}) not found on Cloud.`, { latestVersion, version, }); diff --git a/datastore/core/lib/MicropaymentChannelSpendTracker.ts b/datastore/core/lib/MicropaymentChannelSpendTracker.ts index 8d720a89d..62a31d7ec 100644 --- a/datastore/core/lib/MicropaymentChannelSpendTracker.ts +++ b/datastore/core/lib/MicropaymentChannelSpendTracker.ts @@ -1,10 +1,12 @@ +import { ChannelHold, DomainStore, OpenChannelHold } from '@argonprotocol/localchain'; import Logger from '@ulixee/commons/lib/Logger'; -import { DomainStore, ChannelHold, OpenChannelHold } from '@argonprotocol/localchain'; +import LocalchainWithSync from '@ulixee/datastore/payments/LocalchainWithSync'; import IMicropaymentChannelApiTypes from '@ulixee/platform-specification/services/MicropaymentChannelApis'; import IBalanceChange from '@ulixee/platform-specification/types/IBalanceChange'; -import IDatastoreManifest from '@ulixee/platform-specification/types/IDatastoreManifest'; +import IDatastoreManifest, { + IDatastorePaymentRecipient, +} from '@ulixee/platform-specification/types/IDatastoreManifest'; import serdeJson from '@ulixee/platform-utils/lib/serdeJson'; -import LocalchainWithSync from '@ulixee/datastore/payments/LocalchainWithSync'; import DatastoreChannelHoldsDb from '../db/DatastoreChannelHoldsDb'; import IMicropaymentChannelSpendTracker from '../interfaces/IMicropaymentChannelSpendTracker'; @@ -15,10 +17,19 @@ export default class MicropaymentChannelSpendTracker implements IMicropaymentCha private readonly openChannelHoldsById = new Map(); + private readonly preferredNotaryId: number = 1; constructor( readonly channelHoldDbDir: string, readonly localchain: LocalchainWithSync, - ) {} + ) { + if (localchain?.localchainConfig) { + this.preferredNotaryId = this.localchain.localchainConfig.notaryId; + } + } + + public getPaymentInfo(): Promise { + return this.localchain.paymentInfo.promise; + } public async close(): Promise { return Promise.resolve(); @@ -63,24 +74,26 @@ export default class MicropaymentChannelSpendTracker implements IMicropaymentCha ); } } - if (datastoreManifest.payment.notaryId !== data.channelHold.previousBalanceProof?.notaryId) { + + if ( + this.preferredNotaryId && + this.preferredNotaryId !== data.channelHold.previousBalanceProof?.notaryId + ) { throw new Error( - `The channelHold notary (${data.channelHold.previousBalanceProof?.notaryId}) does not match the required notary (${datastoreManifest.payment.notaryId})`, + `The channelHold notary (${data.channelHold.previousBalanceProof?.notaryId}) does not match the required notary (${this.preferredNotaryId})`, ); } const recipient = note.noteType.recipient; - if (datastoreManifest.payment.address !== recipient) { - throw new Error( - `The datastore payment address (${data.channelHold.accountId}) does not match the ChannelHold recipient (${recipient})`, - ); - } if (!(await this.canSign(recipient))) { - log.warn('This channelHold is made out to a different address than your attached localchain', { - recipient, - channelHold: data.channelHold, - } as any); + log.warn( + 'This channelHold is made out to a different address than your attached localchain', + { + recipient, + channelHold: data.channelHold, + } as any, + ); throw new Error('ChannelHold recipient not localchain address'); } } else { @@ -124,7 +137,8 @@ export default class MicropaymentChannelSpendTracker implements IMicropaymentCha log.stats('Importing channelHold to localchain', { datastoreId, balanceChange } as any); const channelHoldJson = serdeJson(balanceChange); - const openChannelHold = await this.localchain.openChannelHolds.importChannelHold(channelHoldJson); + const openChannelHold = + await this.localchain.openChannelHolds.importChannelHold(channelHoldJson); const channelHold = await openChannelHold.channelHold; this.openChannelHoldsById.set(channelHold.id, openChannelHold); @@ -136,7 +150,8 @@ export default class MicropaymentChannelSpendTracker implements IMicropaymentCha } private getDb(datastoreId: string): DatastoreChannelHoldsDb { - if (!datastoreId) throw new Error('No datastoreId provided to get channelHold spend tracking db.'); + if (!datastoreId) + throw new Error('No datastoreId provided to get channelHold spend tracking db.'); let db = this.channelHoldDbsByDatastore.get(datastoreId); if (!db) { db = new DatastoreChannelHoldsDb(this.channelHoldDbDir, datastoreId); diff --git a/datastore/core/lib/MicropaymentChannelSpendTrackerClient.ts b/datastore/core/lib/MicropaymentChannelSpendTrackerClient.ts index c5c1e94ea..cc1acbeaa 100644 --- a/datastore/core/lib/MicropaymentChannelSpendTrackerClient.ts +++ b/datastore/core/lib/MicropaymentChannelSpendTrackerClient.ts @@ -5,9 +5,18 @@ import IMicropaymentChannelApiTypes, { import IDatastoreManifest from '@ulixee/platform-specification/types/IDatastoreManifest'; import IMicropaymentChannelSpendTracker from '../interfaces/IMicropaymentChannelSpendTracker'; -export default class MicropaymentChannelSpendTrackerClient implements IMicropaymentChannelSpendTracker { +export default class MicropaymentChannelSpendTrackerClient + implements IMicropaymentChannelSpendTracker +{ constructor(readonly serviceClient: ConnectionToCore) {} + public getPaymentInfo(): Promise { + return this.serviceClient.sendRequest({ + command: 'MicropaymentChannel.getPaymentInfo', + args: [], + }); + } + public async debit( data: IMicropaymentChannelApiTypes['MicropaymentChannel.debitPayment']['args'], ): Promise { diff --git a/datastore/core/lib/PaymentsProcessor.ts b/datastore/core/lib/PaymentsProcessor.ts index 491756dee..3fc521a55 100644 --- a/datastore/core/lib/PaymentsProcessor.ts +++ b/datastore/core/lib/PaymentsProcessor.ts @@ -27,7 +27,10 @@ export default class PaymentsProcessor { private payment: IPayment, private datastoreId: string, private datastore: Datastore, - readonly context: Pick, + readonly context: Pick< + IDatastoreApiContext, + 'configuration' | 'micropaymentChannelSpendTracker' + >, ) {} public async debit( @@ -37,7 +40,7 @@ export default class PaymentsProcessor { ): Promise { const price = PricingManager.computePrice(manifest, entityCalls); this.initialPrice = price; - if (price === 0 || !manifest.payment) return true; + if (price === 0) return true; if (!this.payment?.credits?.id && !this.payment?.channelHold?.id) { throw new PaymentRequiredError('This Datastore requires payment.', price); diff --git a/datastore/core/lib/translateDatastoreMetadata.ts b/datastore/core/lib/translateDatastoreMetadata.ts index f6a1f92d5..85ff28884 100644 --- a/datastore/core/lib/translateDatastoreMetadata.ts +++ b/datastore/core/lib/translateDatastoreMetadata.ts @@ -1,5 +1,6 @@ import PricingManager from '@ulixee/datastore/lib/PricingManager'; import { IDatastoreApiTypes } from '@ulixee/platform-specification/datastore'; +import { IDatastorePaymentRecipient } from '@ulixee/platform-specification/types/IDatastoreManifest'; import { IDatastoreStatsRecord } from '../db/DatastoreStatsTable'; import { IDatastoreManifestWithLatest } from '../interfaces/IDatastoreRegistryStore'; import { IDatastoreStats } from './StatsTracker'; @@ -8,6 +9,7 @@ export default async function translateDatastoreMetadata( datastore: IDatastoreManifestWithLatest, datastoreStats: IDatastoreStats, includeSchemaAsJson: boolean, + paymentInfo?: IDatastorePaymentRecipient, ): Promise { const result: IDatastoreApiTypes['Datastore.meta']['result'] = { ...datastore, @@ -15,6 +17,7 @@ export default async function translateDatastoreMetadata( crawlersByName: {}, extractorsByName: {}, tablesByName: {}, + payment: paymentInfo, }; for (const [name, extractor] of Object.entries(datastore.extractorsByName)) { diff --git a/datastore/core/package.json b/datastore/core/package.json index 228584c0e..d97074344 100644 --- a/datastore/core/package.json +++ b/datastore/core/package.json @@ -8,7 +8,7 @@ "@ulixee/commons": "2.0.0-alpha.29", "@ulixee/datastore": "2.0.0-alpha.29", "@ulixee/datastore-docpage": "2.0.0-alpha.29", - "@argonprotocol/localchain": "0.0.6", + "@argonprotocol/localchain": "0.0.8", "@ulixee/net": "2.0.0-alpha.29", "@ulixee/platform-specification": "2.0.0-alpha.29", "@ulixee/platform-utils": "2.0.0-alpha.29", diff --git a/datastore/core/test/Crawler.test.ts b/datastore/core/test/Crawler.test.ts index 09cb22a1e..8933ff4de 100644 --- a/datastore/core/test/Crawler.test.ts +++ b/datastore/core/test/Crawler.test.ts @@ -48,7 +48,7 @@ test('should be able to run a crawler', async () => { const crawler = new DatastorePackager(`${__dirname}/datastores/crawl.js`); await crawler.build(); await client.upload(await crawler.dbx.tarGzip()); - const affiliateId = `aff${nanoid(12)}`; + const affiliateId = `aff${nanoid(10)}`; await expect( client.stream( crawler.manifest.id, @@ -79,7 +79,7 @@ test('should be able to query a crawler', async () => { const { queryLogDb, statsDb } = statsTracker.diskStore; queryLogDb.logTable.db.exec(`delete from ${queryLogDb.logTable.tableName}`); statsDb.datastoreEntities.db.exec(`delete from ${statsDb.datastoreEntities.tableName}`); - const affiliateId = `aff${nanoid(12)}`; + const affiliateId = `aff${nanoid(10)}`; await expect( client.query( crawler.manifest.id, diff --git a/datastore/core/test/Credits.test.ts b/datastore/core/test/Credits.test.ts index 097cdd393..a63d91c6a 100644 --- a/datastore/core/test/Credits.test.ts +++ b/datastore/core/test/Credits.test.ts @@ -1,3 +1,4 @@ +import { Chain, ChainIdentity } from '@argonprotocol/localchain'; import { Keyring } from '@polkadot/keyring'; import { CloudNode } from '@ulixee/cloud'; import UlixeeHostsConfig from '@ulixee/commons/config/hosts'; @@ -27,6 +28,11 @@ const keyring = new Keyring({ ss58Format: 18 }); const datastoreKeyring = keyring.createFromUri('Datastore'); const micropaymentChannelSpendTrackerMock = new MockMicropaymentChannelSpendTracker(); +const mainchainIdentity = { + chain: Chain.Devnet, + genesisHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', +} as ChainIdentity; + beforeAll(async () => { if (Fs.existsSync(`${__dirname}/datastores/output-manifest.json`)) { Fs.unlinkSync(`${__dirname}/datastores/output-manifest.json`); @@ -45,8 +51,16 @@ beforeAll(async () => { }, }, true, + { + address: datastoreKeyring.address, + notaryId: 1, + ...mainchainIdentity, + }, + ); + cloudNode.datastoreCore.micropaymentChannelSpendTracker = new MicropaymentChannelSpendTracker( + storageDir, + null, ); - cloudNode.datastoreCore.micropaymentChannelSpendTracker = new MicropaymentChannelSpendTracker(storageDir, null); client = new DatastoreApiClient(await cloudNode.address, { consoleLogErrors: true }); Helpers.onClose(() => client.disconnect(), true); }); @@ -66,10 +80,6 @@ test('should be able run a Datastore with Credits', async () => { Fs.writeFileSync( `${__dirname}/datastores/output-manifest.json`, JSON.stringify({ - payment: { - address: datastoreKeyring.address, - notaryId: 1, - }, extractorsByName: { putout: { prices: [{ basePrice: 1000 }], @@ -153,6 +163,7 @@ test('should remove an empty Credits from the local cache', async () => { recipient: { address: datastoreKeyring.address, notaryId: 1, + ...mainchainIdentity, }, }), ).resolves.toEqual( @@ -167,6 +178,7 @@ test('should remove an empty Credits from the local cache', async () => { recipient: { address: datastoreKeyring.address, notaryId: 1, + ...mainchainIdentity, }, }), ).rejects.toThrow('Insufficient credits balance'); @@ -177,10 +189,6 @@ test('should be able to embed Credits in a Datastore', async () => { Fs.writeFileSync( `${__dirname}/datastores/output-manifest.json`, JSON.stringify({ - payment: { - address: datastoreKeyring.address, - notaryId: 1, - }, extractorsByName: { putout: { prices: [{ basePrice: 1000 }], @@ -232,10 +240,6 @@ test('should be able to embed Credits in a Datastore', async () => { Fs.writeFileSync( `${__dirname}/datastores/clone-output/datastore-manifest.json`, JSON.stringify({ - payment: { - address: datastoreKeyring.address, - notaryId: 1, - }, extractorsByName: { putout: { prices: [{ basePrice: 1000 }], diff --git a/datastore/core/test/DatastorePayments.test.ts b/datastore/core/test/DatastorePayments.test.ts index 08499d116..7dc757df4 100644 --- a/datastore/core/test/DatastorePayments.test.ts +++ b/datastore/core/test/DatastorePayments.test.ts @@ -1,3 +1,4 @@ +import { Chain, ChainIdentity } from '@argonprotocol/localchain'; import { Keyring } from '@polkadot/keyring'; import { CloudNode } from '@ulixee/cloud'; import UlixeeHostsConfig from '@ulixee/commons/config/hosts'; @@ -25,6 +26,10 @@ const datastoreKeyring = keyring.createFromUri('Datastore'); const micropaymentChannelSpendTrackerMock = new MockMicropaymentChannelSpendTracker(); const micropaymentChannelSpendTracker = new MicropaymentChannelSpendTracker(storageDir, null); let manifest: IDatastoreManifest; +const mainchainIdentity = { + chain: Chain.Devnet, + genesisHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', +} as ChainIdentity; beforeAll(async () => { if (Fs.existsSync(`${__dirname}/datastores/payments-manifest.json`)) { @@ -39,10 +44,6 @@ beforeAll(async () => { `${__dirname}/datastores/payments-manifest.json`, JSON.stringify({ version: '0.0.1', - payment: { - address: datastoreKeyring.address, - notaryId: 1, - }, extractorsByName: { testPayments: { prices: [ @@ -82,6 +83,11 @@ beforeAll(async () => { }, }, true, + { + address: datastoreKeyring.address, + notaryId: 1, + ...mainchainIdentity, + }, ); cloudNode.datastoreCore.micropaymentChannelSpendTracker = micropaymentChannelSpendTracker; client = new DatastoreApiClient(await cloudNode.address, { consoleLogErrors: true }); diff --git a/datastore/core/test/PassthroughExtractors.test.ts b/datastore/core/test/PassthroughExtractors.test.ts index 60d84d60f..60696952c 100644 --- a/datastore/core/test/PassthroughExtractors.test.ts +++ b/datastore/core/test/PassthroughExtractors.test.ts @@ -1,12 +1,13 @@ +import { Chain, ChainIdentity } from '@argonprotocol/localchain'; import { Keyring } from '@polkadot/keyring'; import { CloudNode } from '@ulixee/cloud'; import DatastorePackager from '@ulixee/datastore-packager'; import { Helpers } from '@ulixee/datastore-testing'; import DatastoreApiClient from '@ulixee/datastore/lib/DatastoreApiClient'; import ArgonReserver from '@ulixee/datastore/payments/ArgonReserver'; +import CreditReserver from '@ulixee/datastore/payments/CreditReserver'; import * as Fs from 'fs'; import * as Path from 'path'; -import CreditReserver from '@ulixee/datastore/payments/CreditReserver'; import MicropaymentChannelSpendTracker from '../lib/MicropaymentChannelSpendTracker'; import MockMicropaymentChannelSpendTracker from './_MockMicropaymentChannelSpendTracker'; import MockPaymentService from './_MockPaymentService'; @@ -22,6 +23,11 @@ Helpers.blockGlobalConfigWrites(); const micropaymentChannelSpendTrackerMock = new MockMicropaymentChannelSpendTracker(); const micropaymentChannelSpendTracker = new MicropaymentChannelSpendTracker(storageDir, null); +const mainchainIdentity = { + chain: Chain.Devnet, + genesisHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', +} as ChainIdentity; + let remoteVersion: string; let remoteDatastoreId: string; beforeAll(async () => { @@ -50,6 +56,11 @@ beforeAll(async () => { }, }, true, + { + address: keyring.createFromUri('upstream').address, + notaryId: 1, + ...mainchainIdentity, + }, ); cloudNode.datastoreCore.micropaymentChannelSpendTracker = micropaymentChannelSpendTracker; client = new DatastoreApiClient(await cloudNode.address); @@ -179,10 +190,6 @@ export default new Datastore({ remoteDatastores: { source: 'ulx://${await cloudNode.address}/${remoteDatastoreId}@v${remoteVersion}', }, - payment: { - notaryId: 1, - address: "${new Keyring().createFromUri('upstream').address}", - }, extractors: { pass: new Datastore.PassthroughExtractor({ upcharge: 400, @@ -213,10 +220,55 @@ export default new Datastore({ test('should be able to add charges from multiple extractors', async () => { const address1 = keyring.createFromUri('extractor1'); - let version: string; - let sourceDatastoreId: string; - let hop1DatastoreId: string; + let sourceCloudAddress: string; + let hop1CloudAddress: string; + const sourceDatastoreId = 'source-store'; + const hop1DatastoreId = 'hop1-store'; + + const channelHolds: { datastoreId: string; holdAmount: bigint; channelHoldId: string }[] = []; + const clientAddress = keyring.createFromUri('client'); + const paymentService = new MockPaymentService(clientAddress, client, null, 'client'); + const paymentServices: MockPaymentService[] = [paymentService]; + let hop1Cloud: CloudNode; + let sourceCloudNode: CloudNode; + + micropaymentChannelSpendTrackerMock.mock((id, balanceChange) => { + const channelHoldId = paymentServices + .map(x => x.paymentsByDatastoreId[id]) + .find(Boolean).channelHoldId; + const channelHold = paymentServices.map(x => x.channelHoldsById[channelHoldId]).find(Boolean); + channelHolds.push({ + datastoreId: id, + channelHoldId, + holdAmount: channelHold.channelHoldAmount, + }); + return { + id: channelHoldId, + expirationTick: channelHold.tick + 100, + holdAmount: channelHold.channelHoldAmount, + }; + }); + { + const sourceCloudDir = Path.join(storageDir, 'sourceCloud'); + Fs.mkdirSync(sourceCloudDir, { recursive: true }); + sourceCloudNode = await Helpers.createLocalNode( + { + datastoreConfiguration: { + datastoresDir: sourceCloudDir, + }, + }, + false, + { + address: address1.address, + notaryId: 1, + ...mainchainIdentity, + }, + ); + sourceCloudAddress = await sourceCloudNode.address; + sourceCloudNode.datastoreCore.micropaymentChannelSpendTracker = + new MicropaymentChannelSpendTracker(sourceCloudDir, null); + Fs.writeFileSync( `${__dirname}/datastores/source.js`, ` @@ -224,10 +276,8 @@ const Datastore = require('@ulixee/datastore'); const { boolean, string } = require('@ulixee/schema'); export default new Datastore({ - payment: { - address: '${address1.address}', - notaryId: 1, - }, + id: '${sourceDatastoreId}', + version: '0.0.1', extractors: { source: new Datastore.Extractor({ basePrice: 6, @@ -240,10 +290,9 @@ export default new Datastore({ }, });`, ); - const dbx = new DatastorePackager(`${__dirname}/datastores/source.js`); try { - await dbx.build({ createTemporaryVersion: true }); + await dbx.build(); } catch (err) { console.log('ERROR Uploading Manifest', err); throw err; @@ -252,15 +301,49 @@ export default new Datastore({ Helpers.onClose(() => Fs.promises.rm(`${__dirname}/datastores/source.dbx`, { recursive: true }), ); - await new DatastoreApiClient(await cloudNode.address).upload(await dbx.dbx.tarGzip()); - version = dbx.manifest.version; - sourceDatastoreId = dbx.manifest.id; - expect(dbx.manifest.payment).toBeTruthy(); - const price = await client.pricing.getEntityPrice(sourceDatastoreId, version, 'source'); + const client1 = new DatastoreApiClient(await sourceCloudNode.address); + Helpers.onClose(() => client1.disconnect()); + await client1.upload(await dbx.dbx.tarGzip()); + const price = await client1.pricing.getEntityPrice(sourceDatastoreId, '0.0.1', 'source'); expect(price).toBe(6); } const address2 = keyring.createFromUri('extractor2'); + { + const cloud2StorageDir = Path.join(storageDir, 'hop1Cloud'); + Fs.mkdirSync(cloud2StorageDir, { recursive: true }); + hop1Cloud = await Helpers.createLocalNode( + { + datastoreConfiguration: { + datastoresDir: cloud2StorageDir, + }, + }, + true, + { + address: keyring.createFromUri('extractor2').address, + notaryId: 1, + ...mainchainIdentity, + }, + ); + hop1CloudAddress = await hop1Cloud.address; + hop1Cloud.datastoreCore.micropaymentChannelSpendTracker = new MicropaymentChannelSpendTracker( + cloud2StorageDir, + null, + ); + hop1Cloud.datastoreCore.paymentInfo.resolve({ + address: address2.address, + notaryId: 1, + ...mainchainIdentity, + }); + const corePaymentService = new MockPaymentService( + address2, + new DatastoreApiClient(sourceCloudAddress), + null, + 'hop1cloud', + ); + paymentServices.push(corePaymentService); + mockUpstreamPayments(corePaymentService, hop1Cloud); + Fs.writeFileSync( `${__dirname}/datastores/hop1.js`, ` @@ -268,12 +351,10 @@ const Datastore = require('@ulixee/datastore'); const { boolean, string } = require('@ulixee/schema'); export default new Datastore({ - payment: { - address:'${address2.address}', - notaryId: 1, - }, + id: '${hop1DatastoreId}', + version: '0.0.1', remoteDatastores: { - hop0: 'ulx://${await cloudNode.address}/${sourceDatastoreId}@v${version}', + hop0: 'ulx://${sourceCloudAddress}/${sourceDatastoreId}@v0.0.1', }, extractors: { source2: new Datastore.PassthroughExtractor({ @@ -292,14 +373,13 @@ export default new Datastore({ const dbx = new DatastorePackager(`${__dirname}/datastores/hop1.js`); Helpers.onClose(() => Fs.promises.unlink(`${__dirname}/datastores/hop1.js`)); Helpers.onClose(() => Fs.promises.rm(`${__dirname}/datastores/hop1.dbx`, { recursive: true })); - await dbx.build({ createTemporaryVersion: true }); - await new DatastoreApiClient(await cloudNode.address).upload(await dbx.dbx.tarGzip()); - version = dbx.manifest.version; - hop1DatastoreId = dbx.manifest.id; - const price = await client.pricing.getEntityPrice(hop1DatastoreId, version, 'source2'); + await dbx.build(); + const client2 = new DatastoreApiClient(await hop1Cloud.address); + Helpers.onClose(() => client2.disconnect()); + await client2.upload(await dbx.dbx.tarGzip()); + const price = await client2.pricing.getEntityPrice(hop1DatastoreId, '0.0.1', 'source2'); expect(price).toBe(6 + 11); } - const address3 = keyring.createFromUri('extractor3'); Fs.writeFileSync( `${__dirname}/datastores/hop2.js`, ` @@ -307,12 +387,8 @@ const Datastore = require('@ulixee/datastore'); const { boolean, string } = require('@ulixee/schema'); export default new Datastore({ - payment: { - address:'${address3.address}', - notaryId: 1, - }, remoteDatastores: { - hop1: 'ulx://${await cloudNode.address}/${hop1DatastoreId}@v${version}', + hop1: 'ulx://${hop1CloudAddress}/${hop1DatastoreId}@v0.0.1', }, extractors: { last: new Datastore.PassthroughExtractor({ @@ -348,31 +424,14 @@ export default new Datastore({ ), ).resolves.toBe(price); - const clientAddress = keyring.createFromUri('client'); - const paymentService = new MockPaymentService(clientAddress, client); - const client2 = keyring.createFromUri('upstream'); const corePaymentService = new MockPaymentService( - client2, - cloudNode.datastoreCore.datastoreApiClients.get(await cloudNode.address), + keyring.createFromUri('upstream'), + new DatastoreApiClient(hop1CloudAddress), + null, + 'hop2cloud', ); - const channelHolds: { datastoreId: string; holdAmount: bigint; channelHoldId: string }[] = []; - micropaymentChannelSpendTrackerMock.mock((id, balanceChange) => { - const channelHoldId = ( - paymentService.paymentsByDatastoreId[id] ?? corePaymentService.paymentsByDatastoreId[id] - ).channelHoldId; - const channelHold = paymentService.channelHoldsById[channelHoldId] ?? corePaymentService.channelHoldsById[channelHoldId]; - channelHolds.push({ datastoreId: id, channelHoldId, holdAmount: channelHold.channelHoldAmount }); - return { - id: channelHoldId, - expirationTick: channelHold.tick + 100, - holdAmount: channelHold.channelHoldAmount, - }; - }); - - cloudNode.datastoreCore.upstreamDatastorePaymentService = corePaymentService; - // @ts-expect-error - cloudNode.datastoreCore.vm.remotePaymentService = - cloudNode.datastoreCore.upstreamDatastorePaymentService; + paymentServices.push(corePaymentService); + mockUpstreamPayments(corePaymentService, cloudNode); const result = await client.query( lastHop.manifest.id, lastHop.manifest.version, @@ -387,47 +446,72 @@ export default new Datastore({ expect(channelHolds).toHaveLength(3); // @ts-expect-error - const dbs = micropaymentChannelSpendTracker.channelHoldDbsByDatastore; - expect(dbs.size).toBe(3); - // @ts-expect-error + let dbs = sourceCloudNode.datastoreCore.micropaymentChannelSpendTracker.channelHoldDbsByDatastore; + expect(dbs.size).toBe(1); + + const paymentsByDatastoreId = {}; + for (const service of paymentServices) { + Object.assign(paymentsByDatastoreId, service.paymentsByDatastoreId); + } expect(dbs.get(sourceDatastoreId).paymentIdByChannelHoldId.size).toBe(1); expect(dbs.get(sourceDatastoreId).list()).toEqual([ expect.objectContaining({ - id: corePaymentService.paymentsByDatastoreId[sourceDatastoreId].channelHoldId, + id: paymentsByDatastoreId[sourceDatastoreId].channelHoldId, allocated: 1000, remaining: 1000 - 6, }), ]); // @ts-expect-error + dbs = hop1Cloud.datastoreCore.micropaymentChannelSpendTracker.channelHoldDbsByDatastore; + expect(dbs.size).toBe(1); + expect(dbs.get(hop1DatastoreId).paymentIdByChannelHoldId.size).toBe(1); expect(dbs.get(hop1DatastoreId).list()).toEqual([ expect.objectContaining({ - id: corePaymentService.paymentsByDatastoreId[hop1DatastoreId].channelHoldId, + id: paymentsByDatastoreId[hop1DatastoreId].channelHoldId, allocated: 2000, remaining: 2000 - 6 - 11, }), ]); + // @ts-expect-error + dbs = micropaymentChannelSpendTracker.channelHoldDbsByDatastore; + expect(dbs.size).toBe(1); + expect(dbs.get(hop2DatastoreId).paymentIdByChannelHoldId.size).toBe(1); expect(dbs.get(hop2DatastoreId).list()).toEqual([ expect.objectContaining({ - id: paymentService.paymentsByDatastoreId[hop2DatastoreId].channelHoldId, + id: paymentsByDatastoreId[hop2DatastoreId].channelHoldId, allocated: 2000, remaining: 2000 - 6 - 11 - 3, }), ]); - const queryLog = cloudNode.datastoreCore.statsTracker.diskStore.queryLogDb.logTable + let queryLog = sourceCloudNode.datastoreCore.statsTracker.diskStore.queryLogDb.logTable .all() .filter(x => [sourceDatastoreId, hop1DatastoreId, hop2DatastoreId].includes(x.datastoreId)); - expect(queryLog).toHaveLength(3); + expect(queryLog).toHaveLength(1); const queryId = result.queryId; expect(queryLog[0].queryId).toBe(`${queryId}.1.1`); - expect(queryLog[1].queryId).toBe(`${queryId}.1`); - expect(queryLog[2].queryId).toBe(queryId); - expect(queryLog.map(x => x.datastoreId)).toEqual([ - sourceDatastoreId, - hop1DatastoreId, - hop2DatastoreId, - ]); + expect(queryLog[0].datastoreId).toBe(sourceDatastoreId); + + queryLog = hop1Cloud.datastoreCore.statsTracker.diskStore.queryLogDb.logTable + .all() + .filter(x => [sourceDatastoreId, hop1DatastoreId, hop2DatastoreId].includes(x.datastoreId)); + expect(queryLog).toHaveLength(1); + expect(queryLog[0].queryId).toBe(`${queryId}.1`); + + queryLog = cloudNode.datastoreCore.statsTracker.diskStore.queryLogDb.logTable + .all() + .filter(x => [sourceDatastoreId, hop1DatastoreId, hop2DatastoreId].includes(x.datastoreId)); + expect(queryLog[0].queryId).toBe(queryId); }); + +function mockUpstreamPayments( + corePaymentService: MockPaymentService, + registerOnCloudNode: CloudNode, +) { + registerOnCloudNode.datastoreCore.upstreamDatastorePaymentService = corePaymentService; + // @ts-expect-error + registerOnCloudNode.datastoreCore.vm.remotePaymentService = corePaymentService; +} diff --git a/datastore/core/test/_MockPaymentService.ts b/datastore/core/test/_MockPaymentService.ts index 0dc1e41e9..cc335d63d 100644 --- a/datastore/core/test/_MockPaymentService.ts +++ b/datastore/core/test/_MockPaymentService.ts @@ -8,6 +8,7 @@ import { IPayment } from '@ulixee/platform-specification'; import { AccountType } from '@ulixee/platform-specification/types/IBalanceChange'; import IPaymentServiceApiTypes from '@ulixee/platform-specification/datastore/PaymentServiceApis'; import { nanoid } from 'nanoid'; +import { IDatastorePaymentRecipient } from '@ulixee/platform-specification/types/IDatastoreManifest'; export default class MockPaymentService extends TypedEventEmitter @@ -31,6 +32,8 @@ export default class MockPaymentService constructor( public clientAddress: KeyringPair, public client: DatastoreApiClient, + public paymentInfo?: IDatastorePaymentRecipient, + private name?: string, ) { super(); } @@ -39,6 +42,10 @@ export default class MockPaymentService return null; } + async getPaymentInfo(): Promise { + return this.paymentInfo; + } + async reserve( info: IPaymentServiceApiTypes['PaymentService.reserve']['args'], ): Promise { diff --git a/datastore/main/cli/cloneDatastore.ts b/datastore/main/cli/cloneDatastore.ts index 3956f924c..e2af98ca1 100644 --- a/datastore/main/cli/cloneDatastore.ts +++ b/datastore/main/cli/cloneDatastore.ts @@ -105,7 +105,7 @@ export default async function cloneDatastore( id: ${JSON.stringify(meta.id + 2)}, version: "0.0.1", name: ${JSON.stringify(meta.name)},${description} - affiliateId: "aff${nanoid(12)}", + affiliateId: "aff${nanoid(10)}", remoteDatastores: { source: "${url}", },${remoteCredits} diff --git a/datastore/main/interfaces/ILocalchainConfig.ts b/datastore/main/interfaces/ILocalchainConfig.ts index 858bca51b..e183814a0 100644 --- a/datastore/main/interfaces/ILocalchainConfig.ts +++ b/datastore/main/interfaces/ILocalchainConfig.ts @@ -19,7 +19,7 @@ export default interface ILocalchainConfig { keystorePassword?: KeystorePasswordOption; /** - * Should sync be run automatically, or wait for programmatic execution + * Should sync be run automatically, or wait for programmatic execution (default false) */ - automaticallyRunSync?: boolean; + disableAutomaticSync?: boolean; } diff --git a/datastore/main/interfaces/IPaymentService.ts b/datastore/main/interfaces/IPaymentService.ts index 4139bce6c..93883e6b7 100644 --- a/datastore/main/interfaces/IPaymentService.ts +++ b/datastore/main/interfaces/IPaymentService.ts @@ -1,5 +1,5 @@ +import { BalanceChangeStatus, LocalchainOverview } from '@argonprotocol/localchain'; import ITypedEventEmitter from '@ulixee/commons/interfaces/ITypedEventEmitter'; -import { LocalchainOverview, BalanceChangeStatus } from '@argonprotocol/localchain'; import { IPayment } from '@ulixee/platform-specification'; import IPaymentServiceApiTypes from '@ulixee/platform-specification/datastore/PaymentServiceApis'; import { IPaymentMethod } from '@ulixee/platform-specification/types/IPayment'; @@ -24,7 +24,6 @@ export interface IPaymentEvents { } export interface IPaymentReserver extends ITypedEventEmitter { - datastoreLookup?: IDatastoreHostLookup; reserve(info: IPaymentServiceApiTypes['PaymentService.reserve']['args']): Promise; finalize(info: IPaymentServiceApiTypes['PaymentService.finalize']['args']): Promise; close(): Promise; diff --git a/datastore/main/lib/DatastoreApiClient.ts b/datastore/main/lib/DatastoreApiClient.ts index debef1b3f..fbc7914f9 100644 --- a/datastore/main/lib/DatastoreApiClient.ts +++ b/datastore/main/lib/DatastoreApiClient.ts @@ -176,14 +176,13 @@ export default class DatastoreApiClient { (async () => { const price = await this.pricing.getEntityPrice(id, version, name); try { - const paymentInfo = (await this.getMeta(id, version)).payment; - if (!paymentInfo || price <= 0) paymentService = null; + if (price <= 0) paymentService = null; const payment = await paymentService?.reserve({ host, id, version, microgons: price, - recipient: paymentInfo, + recipient: (await this.getMeta(id, version)).payment, domain: options.domain, }); if (payment) { @@ -236,7 +235,7 @@ export default class DatastoreApiClient { const host = this.connectionToCore.transport.host; const paymentInfo = (await this.getMeta(id, version)).payment; let paymentService = options.paymentService; - if (!paymentInfo || price <= 0) paymentService = null; + if (price <= 0) paymentService = null; const payment = await paymentService?.reserve({ id, @@ -478,7 +477,9 @@ export default class DatastoreApiClient { datastoreUrl: string, argonMainchainUrl: string, ): Promise { - const mainchainClient = argonMainchainUrl ? await MainchainClient.connect(argonMainchainUrl, 10e3) : null; + const mainchainClient = argonMainchainUrl + ? await MainchainClient.connect(argonMainchainUrl, 10e3) + : null; try { return await new DatastoreLookup(mainchainClient).getHostInfo(datastoreUrl); } finally { diff --git a/datastore/main/lib/DatastoreLookup.ts b/datastore/main/lib/DatastoreLookup.ts index 9f49337a0..7dd6988ac 100644 --- a/datastore/main/lib/DatastoreLookup.ts +++ b/datastore/main/lib/DatastoreLookup.ts @@ -1,6 +1,6 @@ import TimedCache from '@ulixee/commons/lib/TimedCache'; import { bindFunctions, toUrl } from '@ulixee/commons/lib/utils'; -import { Domain, DomainTopLevel, ZoneRecord } from '@argonprotocol/localchain'; +import { ChainIdentity, Domain, DomainTopLevel, ZoneRecord } from '@argonprotocol/localchain'; import { datastoreRegex } from '@ulixee/platform-specification/types/datastoreIdValidation'; import { semverRegex } from '@ulixee/platform-specification/types/semverValidation'; import * as net from 'node:net'; @@ -10,6 +10,7 @@ export { DomainTopLevel }; export interface IZoneRecordLookup { getDomainZoneRecord(domainName: string, tld: DomainTopLevel): Promise; + getChainIdentity(): Promise; } /** * Singleton that will track payments for each channelHold for a datastore @@ -19,6 +20,8 @@ export default class DatastoreLookup implements IDatastoreHostLookup { [domain: string]: TimedCache; } = {}; + private chainIdentity: Promise; + constructor(private mainchainClient: IZoneRecordLookup) { bindFunctions(this); } @@ -33,7 +36,27 @@ export default class DatastoreLookup implements IDatastoreHostLookup { return await this.lookupDatastoreDomain(url.host, version); } - public async lookupDatastoreDomain(domain: string, version: string): Promise { + public async validatePayment(paymentInfo: { + recipient?: { address?: string; notaryId?: number }; + domain?: string; + }): Promise { + if (paymentInfo.domain && paymentInfo.recipient) { + const zoneRecord = await this.lookupDatastoreDomain(paymentInfo.domain, 'any'); + if (zoneRecord) { + if (zoneRecord.payment.notaryId !== paymentInfo.recipient.notaryId) { + throw new Error('Payment notaryId does not match Domain record'); + } + if (zoneRecord.payment.address !== paymentInfo.recipient.address) { + throw new Error('Payment address does not match Domain record'); + } + } + } + } + + public async lookupDatastoreDomain( + domain: string, + version: string | 'any', + ): Promise { let zoneRecord = this.zoneRecordByDomain[domain]?.value; if (!zoneRecord) { if (!this.mainchainClient) @@ -43,6 +66,8 @@ export default class DatastoreLookup implements IDatastoreHostLookup { const parsed = DatastoreLookup.readDomain(domain); const zone = await this.mainchainClient.getDomainZoneRecord(parsed.name, parsed.topLevel); + if (!zone) throw new Error(`Zone record for Domain (${domain}) not found`); + this.zoneRecordByDomain[domain] ??= new TimedCache(24 * 60 * 60e3); this.zoneRecordByDomain[domain].value = { ...zone, @@ -51,13 +76,25 @@ export default class DatastoreLookup implements IDatastoreHostLookup { zoneRecord = this.zoneRecordByDomain[domain].value; } - const versionHost = zoneRecord.versions[version]; + let versionHost = zoneRecord.versions[version]; + if (!versionHost && version === 'any') { + versionHost = Object.values(zoneRecord.versions)[0]; + } + if (!versionHost) throw new Error('Version not found'); + + this.chainIdentity ??= this.mainchainClient.getChainIdentity(); + const chainIdentity = await this.chainIdentity; return { datastoreId: versionHost.datastoreId, host: versionHost.host, version, domain, + payment: { + address: zoneRecord.paymentAddress, + notaryId: zoneRecord.notaryId, + ...chainIdentity, + }, }; } diff --git a/datastore/main/package.json b/datastore/main/package.json index 2799e0f35..26c4d5f2f 100644 --- a/datastore/main/package.json +++ b/datastore/main/package.json @@ -43,7 +43,7 @@ "@polkadot/keyring": "^13.1.1", "@polkadot/util-crypto": "^13.1.1", "@ulixee/commons": "2.0.0-alpha.29", - "@argonprotocol/localchain": "0.0.6", + "@argonprotocol/localchain": "0.0.8", "@ulixee/net": "2.0.0-alpha.29", "@ulixee/platform-specification": "2.0.0-alpha.29", "@ulixee/platform-utils": "2.0.0-alpha.29", diff --git a/datastore/main/payments/ArgonReserver.ts b/datastore/main/payments/ArgonReserver.ts index c4cee84fa..583acfe7c 100644 --- a/datastore/main/payments/ArgonReserver.ts +++ b/datastore/main/payments/ArgonReserver.ts @@ -12,7 +12,6 @@ import IBalanceChange from '@ulixee/platform-specification/types/IBalanceChange' import ArgonUtils from '@ulixee/platform-utils/lib/ArgonUtils'; import { nanoid } from 'nanoid'; import * as Path from 'node:path'; -import IDatastoreHostLookup from '../interfaces/IDatastoreHostLookup'; import { IPaymentDetails, IPaymentEvents, IPaymentReserver } from '../interfaces/IPaymentService'; import DatastoreApiClients from '../lib/DatastoreApiClients'; @@ -32,7 +31,6 @@ export interface IChannelHoldDetails { export interface IChannelHoldSource { sourceKey: string; - datastoreLookup?: IDatastoreHostLookup; createChannelHold( paymentInfo: IPaymentServiceApiTypes['PaymentService.reserve']['args'], milligons: bigint, @@ -50,8 +48,6 @@ export default class ArgonReserver public static baseStorePath = Path.join(getDataDirectory(), `ulixee`); public readonly paymentsByDatastoreId: IPaymentDetailsByDatastoreId = {}; - public datastoreLookup?: IDatastoreHostLookup; - private paymentsPendingFinalization: { [uuid: string]: { microgons: number; datastoreId: string; paymentId: string }; } = {}; @@ -78,7 +74,6 @@ export default class ArgonReserver super(); this.storePath = Path.join(ArgonReserver.baseStorePath, `${channelHoldSource.sourceKey}.json`); this.saveInterval = setInterval(() => this.save(), 5e3).unref(); - this.datastoreLookup = channelHoldSource.datastoreLookup; if (!apiClients) { this.apiClients = new DatastoreApiClients(); this.closeApiClients = true; diff --git a/datastore/main/payments/BrokerChannelHoldSource.ts b/datastore/main/payments/BrokerChannelHoldSource.ts index 5bfe20326..d8c95f22a 100644 --- a/datastore/main/payments/BrokerChannelHoldSource.ts +++ b/datastore/main/payments/BrokerChannelHoldSource.ts @@ -1,9 +1,9 @@ +import { ADDRESS_PREFIX, BalanceChangeBuilder } from '@argonprotocol/localchain'; import { Keyring } from '@polkadot/keyring'; import { mnemonicGenerate } from '@polkadot/util-crypto'; import { concatAsBuffer } from '@ulixee/commons/lib/bufferUtils'; import { sha256 } from '@ulixee/commons/lib/hashUtils'; import { toUrl } from '@ulixee/commons/lib/utils'; -import { ADDRESS_PREFIX, BalanceChangeBuilder } from '@argonprotocol/localchain'; import { ConnectionToCore } from '@ulixee/net'; import HttpTransportToCore from '@ulixee/net/lib/HttpTransportToCore'; import { IDatabrokerApis } from '@ulixee/platform-specification/datastore'; diff --git a/datastore/main/payments/CreditReserver.ts b/datastore/main/payments/CreditReserver.ts index ea23878ac..3a3ac7686 100644 --- a/datastore/main/payments/CreditReserver.ts +++ b/datastore/main/payments/CreditReserver.ts @@ -8,6 +8,7 @@ import TypeSerializer from '@ulixee/commons/lib/TypeSerializer'; import { toUrl } from '@ulixee/commons/lib/utils'; import { IPayment } from '@ulixee/platform-specification'; import IPaymentServiceApiTypes from '@ulixee/platform-specification/datastore/PaymentServiceApis'; +import { IDatastorePaymentRecipient } from '@ulixee/platform-specification/types/IDatastoreManifest'; import { IPaymentMethod } from '@ulixee/platform-specification/types/IPayment'; import { nanoid } from 'nanoid'; import { mkdir, readdir, unlink } from 'node:fs/promises'; @@ -69,6 +70,10 @@ export default class CreditReserver this.saveDebounce = debounce(this.save.bind(this), 1_000, 5_000); } + public async getPaymentInfo(): Promise { + return undefined; + } + public hasBalance(microgons: number): boolean { return this.paymentDetails.remaining >= microgons; } diff --git a/datastore/main/payments/DefaultPaymentService.ts b/datastore/main/payments/DefaultPaymentService.ts index 0645d5f13..ef07d4aed 100644 --- a/datastore/main/payments/DefaultPaymentService.ts +++ b/datastore/main/payments/DefaultPaymentService.ts @@ -1,5 +1,5 @@ -import { TypedEventEmitter } from '@ulixee/commons/lib/eventUtils'; import { MainchainClient } from '@argonprotocol/localchain'; +import { TypedEventEmitter } from '@ulixee/commons/lib/eventUtils'; import { IPayment } from '@ulixee/platform-specification'; import IPaymentServiceApiTypes from '@ulixee/platform-specification/datastore/PaymentServiceApis'; import { IPaymentMethod } from '@ulixee/platform-specification/types/IPayment'; @@ -33,8 +33,6 @@ export default class DefaultPaymentService private readonly paymentUuidToService: { [uuid: string]: WeakRef } = {}; private readonly creditsAutoLoaded: Promise; - #datastoreLookup?: IDatastoreHostLookup; - constructor( argonReserver?: IPaymentReserver, loadCreditFromPath: string | 'default' = 'default', @@ -93,24 +91,25 @@ export default class DefaultPaymentService credit: IPaymentMethod['credits'], datastoreLookup?: IDatastoreHostLookup, ): Promise { - this.#datastoreLookup ??= datastoreLookup ?? (await this.argonReserver.datastoreLookup); - if (!this.#datastoreLookup && Env.argonMainchainUrl) { - const mainchainClient = await MainchainClient.connect(Env.argonMainchainUrl, 10e3); - this.#datastoreLookup = new DatastoreLookup(mainchainClient); + let mainchainClientToClose: MainchainClient; + if (!datastoreLookup && Env.argonMainchainUrl) { + mainchainClientToClose = await MainchainClient.connect(Env.argonMainchainUrl, 10e3); + datastoreLookup = new DatastoreLookup(mainchainClientToClose); + } + try { + const service = await CreditReserver.lookup(url, credit, datastoreLookup, this.creditsPath); + this.addCredit(service); + } finally { + if (mainchainClientToClose) { + await mainchainClientToClose.close(); + } } - const service = await CreditReserver.lookup( - url, - credit, - this.#datastoreLookup, - this.creditsPath, - ); - this.addCredit(service); } public async reserve( info: IPaymentServiceApiTypes['PaymentService.reserve']['args'], ): Promise { - if (!info.microgons || !info.recipient) return null; + if (!info.microgons) return null; await this.creditsAutoLoaded; let datastoreCredits = 0; @@ -134,6 +133,11 @@ export default class DefaultPaymentService "You don't have any valid payment methods configured. Please install any credits you have or connect a localchain.", ); } + if (!info.recipient) { + throw new Error( + "This Datastore hasn't configured a payment address, so it can't receive Argons as payment.", + ); + } const payment = await this.argonReserver?.reserve(info); if (payment) { this.paymentUuidToService[payment.uuid] = new WeakRef(this.argonReserver); @@ -156,8 +160,17 @@ export default class DefaultPaymentService apiClients?: DatastoreApiClients, loadCreditsFromPath?: string | 'default', ): Promise { - const channelHoldSource = new LocalchainChannelHoldSource(localchain, await localchain.address); - const reserver = new ArgonReserver(channelHoldSource, channelHoldAllocationStrategy, apiClients); + const datastoreLookup = new DatastoreLookup(await localchain.mainchainClient); + const channelHoldSource = new LocalchainChannelHoldSource( + localchain, + await localchain.address, + datastoreLookup, + ); + const reserver = new ArgonReserver( + channelHoldSource, + channelHoldAllocationStrategy, + apiClients, + ); await reserver.load(); return new DefaultPaymentService(reserver, loadCreditsFromPath); } @@ -174,7 +187,11 @@ export default class DefaultPaymentService identityConfig.passphrase ? { keyPassphrase: identityConfig.passphrase } : undefined, ); const channelHoldSource = new BrokerChannelHoldSource(brokerHost, identity); - const reserver = new ArgonReserver(channelHoldSource, channelHoldAllocationStrategy, apiClients); + const reserver = new ArgonReserver( + channelHoldSource, + channelHoldAllocationStrategy, + apiClients, + ); await reserver.load(); return new DefaultPaymentService(reserver, loadCreditsFromPath); } diff --git a/datastore/main/payments/LocalchainChannelHoldSource.ts b/datastore/main/payments/LocalchainChannelHoldSource.ts index ddcccacf2..3daedb773 100644 --- a/datastore/main/payments/LocalchainChannelHoldSource.ts +++ b/datastore/main/payments/LocalchainChannelHoldSource.ts @@ -3,6 +3,7 @@ import IPaymentServiceApiTypes from '@ulixee/platform-specification/datastore/Pa import IBalanceChange, { BalanceChangeSchema, } from '@ulixee/platform-specification/types/IBalanceChange'; +import DatastoreLookup from '../lib/DatastoreLookup'; import { IChannelHoldDetails, IChannelHoldSource } from './ArgonReserver'; import LocalchainWithSync from './LocalchainWithSync'; @@ -16,6 +17,7 @@ export default class LocalchainChannelHoldSource implements IChannelHoldSource { constructor( public localchain: LocalchainWithSync, public address: string, + public datastoreLookup: DatastoreLookup, ) {} public async getAccountOverview(): Promise { @@ -27,6 +29,9 @@ export default class LocalchainChannelHoldSource implements IChannelHoldSource { milligons: bigint, ): Promise { const { domain } = paymentInfo; + if (domain) { + await this.datastoreLookup.validatePayment(paymentInfo); + } const openChannelHold = await this.localchain.inner.transactions.createChannelHold( milligons, paymentInfo.recipient.address!, @@ -55,7 +60,8 @@ export default class LocalchainChannelHoldSource implements IChannelHoldSource { updatedSettlement: bigint, ): Promise { const channelHoldId = channelHold.channelHoldId; - this.openChannelHoldsById[channelHoldId] ??= await this.localchain.openChannelHolds.get(channelHoldId); + this.openChannelHoldsById[channelHoldId] ??= + await this.localchain.openChannelHolds.get(channelHoldId); const openChannelHold = this.openChannelHoldsById[channelHoldId]; const result = await openChannelHold.sign(updatedSettlement); const balanceChange = channelHold.balanceChange; diff --git a/datastore/main/payments/LocalchainWithSync.ts b/datastore/main/payments/LocalchainWithSync.ts index 3e1e13c8d..e4c70f133 100644 --- a/datastore/main/payments/LocalchainWithSync.ts +++ b/datastore/main/payments/LocalchainWithSync.ts @@ -1,5 +1,3 @@ -import { TypedEventEmitter } from '@ulixee/commons/lib/eventUtils'; -import Logger from '@ulixee/commons/lib/Logger'; import { BalanceSync, BalanceSyncResult, @@ -12,6 +10,10 @@ import { OpenChannelHoldsStore, Transactions, } from '@argonprotocol/localchain'; +import { TypedEventEmitter } from '@ulixee/commons/lib/eventUtils'; +import Logger from '@ulixee/commons/lib/Logger'; +import Resolvable from '@ulixee/commons/lib/Resolvable'; +import { IDatastorePaymentRecipient } from '@ulixee/platform-specification/types/IDatastoreManifest'; import { gettersToObject } from '@ulixee/platform-utils/lib/objectUtils'; import * as Path from 'node:path'; import Env from '../env'; @@ -47,6 +49,10 @@ export default class LocalchainWithSync extends TypedEventEmitter<{ sync: Balanc return this.#localchain.mainchainTransfers; } + public get mainchainClient(): Promise { + return this.#localchain.mainchainClient; + } + public get inner(): Localchain { return this.#localchain; } @@ -55,6 +61,7 @@ export default class LocalchainWithSync extends TypedEventEmitter<{ sync: Balanc public datastoreLookup!: DatastoreLookup; public address!: Promise; public enableLogging = true; + public paymentInfo = new Resolvable(); private nextTick: NodeJS.Timeout; @@ -141,14 +148,25 @@ export default class LocalchainWithSync extends TypedEventEmitter<{ sync: Balanc } private afterLoad(): void { - const { keystorePassword } = this.localchainConfig; + const { keystorePassword, notaryId } = this.localchainConfig; this.address = this.#localchain.address.catch(() => null); // remove password from memory if (Buffer.isBuffer(keystorePassword?.password)) { keystorePassword.password.fill(0); delete keystorePassword.password; } - if (this.localchainConfig.automaticallyRunSync !== false) this.scheduleNextTick(); + void this.getAccountOverview() + // eslint-disable-next-line promise/always-return + .then(x => { + this.paymentInfo.resolve({ + address: x.address, + ...x.mainchainIdentity, + notaryId, + }); + }) + .catch(this.paymentInfo.reject); + + if (this.localchainConfig.disableAutomaticSync !== true) this.scheduleNextTick(); } private scheduleNextTick(): void { diff --git a/datastore/packager/index.ts b/datastore/packager/index.ts index 004486f42..55e8b2bf5 100644 --- a/datastore/packager/index.ts +++ b/datastore/packager/index.ts @@ -225,11 +225,6 @@ export default class DatastorePackager extends TypedEventEmitter<{ build: void } ]) { if (entity.prices?.some(x => x.basePrice > 0)) entitiesNeedingPayment.push(name); } - if (!this.manifest.payment && entitiesNeedingPayment.length) { - throw new Error( - `Datastore ${this.manifest.id} requires payment for entities (${entitiesNeedingPayment.join(', ')}), but no payment details are in the manifest.`, - ); - } this.script = sourceCode; this.sourceMap = sourceMap; diff --git a/datastore/packager/test/Packager.test.ts b/datastore/packager/test/Packager.test.ts index 99ae711e1..abd499755 100644 --- a/datastore/packager/test/Packager.test.ts +++ b/datastore/packager/test/Packager.test.ts @@ -58,7 +58,6 @@ test('it should generate a relative script entrypoint', async () => { id: packager.manifest.id, version: '0.0.1', versionTimestamp: expect.any(Number), - payment: undefined, }); expect((await Fs.stat(`${__dirname}/assets/historyTest.dbx`)).isDirectory()).toBeTruthy(); @@ -167,7 +166,6 @@ test('should be able to package a multi-function Datastore', async () => { id: packager.manifest.id, version: packager.manifest.version, versionTimestamp: expect.any(Number), - payment: undefined, }); expect((await Fs.stat(`${__dirname}/assets/multiExtractorTest.dbx`)).isDirectory()).toBeTruthy(); @@ -204,7 +202,6 @@ test('should be able to package an exported Extractor without a Datastore', asyn id: packager.manifest.id, version: packager.manifest.version, versionTimestamp: expect.any(Number), - payment: undefined, adminIdentities: [], }); expect((await Fs.stat(`${__dirname}/assets/rawExtractorTest.dbx`)).isDirectory()).toBeTruthy(); diff --git a/datastore/testing/helpers.ts b/datastore/testing/helpers.ts index d2879a726..55f72ef9a 100644 --- a/datastore/testing/helpers.ts +++ b/datastore/testing/helpers.ts @@ -5,6 +5,7 @@ import { CanceledPromiseError } from '@ulixee/commons/interfaces/IPendingWaitEve import Logger from '@ulixee/commons/lib/Logger'; import DatastoreManifest from '@ulixee/datastore-core/lib/DatastoreManifest'; import Core from '@ulixee/hero-core'; +import type { IDatastorePaymentRecipient } from '@ulixee/platform-specification/types/IDatastoreManifest'; import * as Fs from 'fs/promises'; import * as http from 'http'; import { Server } from 'http'; @@ -40,6 +41,7 @@ export const needsClosing: { close: () => Promise | void; onlyCloseOnFinal? export async function createLocalNode( config: ConstructorParameters[0], onlyCloseOnFinal = false, + paymentInfo?: IDatastorePaymentRecipient, ): Promise { const datastoreConfig = config.datastoreConfiguration; if (datastoreConfig.datastoresDir) { @@ -54,6 +56,9 @@ export async function createLocalNode( const cloudNode = new CloudNode(config); onClose(() => cloudNode.close(), onlyCloseOnFinal); + if (paymentInfo) { + cloudNode.datastoreCore.paymentInfo.resolve(paymentInfo); + } await cloudNode.listen(); return cloudNode; } diff --git a/desktop/interfaces/package.json b/desktop/interfaces/package.json index aa5a84746..d6ebe0594 100644 --- a/desktop/interfaces/package.json +++ b/desktop/interfaces/package.json @@ -11,6 +11,6 @@ "nanoid": "^3.3.6" }, "devDependencies": { - "@argonprotocol/localchain": "0.0.6" + "@argonprotocol/localchain": "0.0.8" } } diff --git a/end-to-end/package.json b/end-to-end/package.json index c33bf1675..1eeb7557f 100644 --- a/end-to-end/package.json +++ b/end-to-end/package.json @@ -13,8 +13,8 @@ "@ulixee/datastore": "2.0.0-alpha.29", "@ulixee/datastore-packager": "2.0.0-alpha.29", "@ulixee/datastore-testing": "2.0.0-alpha.29", - "@argonprotocol/localchain": "0.0.6", - "@argonprotocol/mainchain": "0.0.6", + "@argonprotocol/localchain": "0.0.8", + "@argonprotocol/mainchain": "0.0.8", "@ulixee/net": "2.0.0-alpha.29", "@ulixee/platform-specification": "2.0.0-alpha.29", "@ulixee/platform-utils": "2.0.0-alpha.29", diff --git a/end-to-end/test/credits.e2e.test.ts b/end-to-end/test/credits.e2e.test.ts index f316e9e51..70629688c 100644 --- a/end-to-end/test/credits.e2e.test.ts +++ b/end-to-end/test/credits.e2e.test.ts @@ -1,9 +1,6 @@ -import { Keyring } from '@polkadot/keyring'; import { Helpers } from '@ulixee/datastore-testing'; import DatastoreApiClient from '@ulixee/datastore/lib/DatastoreApiClient'; import DefaultPaymentService from '@ulixee/datastore/payments/DefaultPaymentService'; -import IDatastoreManifest from '@ulixee/platform-specification/types/IDatastoreManifest'; -import { writeFile } from 'node:fs/promises'; import * as Path from 'node:path'; import TestCloudNode from '../lib/TestCloudNode'; import { execAndLog, getPlatformBuild } from '../lib/utils'; @@ -27,19 +24,11 @@ test('it can create a datastore with credits using cli', async () => { ULX_CLOUD_ADMIN_IDENTITIES: identityBech32.trim(), ULX_DATASTORE_DIR: storageDir, ULX_IDENTITY_PATH: identityPath, + // ULX_PAYMENT_ADDRESS: }); expect(cloudAddress).toBeTruthy(); const datastorePath = Path.join('end-to-end', 'test', 'datastore', 'credits.js'); - await writeFile( - Path.join(buildDir, datastorePath.replace('.js', '-manifest.json')), - JSON.stringify(>{ - payment: { - notaryId: 1, - address: new Keyring().createFromUri('//Alice').address, - }, - }), - ); execAndLog( `npx @ulixee/datastore deploy --skip-docs -h ${cloudAddress} .${Path.sep}${datastorePath}`, { diff --git a/end-to-end/test/payments-broker.e2e.test.ts b/end-to-end/test/payments-broker.e2e.test.ts index 386b69a99..0ba51ebdb 100644 --- a/end-to-end/test/payments-broker.e2e.test.ts +++ b/end-to-end/test/payments-broker.e2e.test.ts @@ -194,10 +194,6 @@ async function setupDatastore( cloudAddress, { domain, - payment: { - notaryId: 1, - address: domainOwner.address, - }, }, identityPath, ); diff --git a/end-to-end/test/payments.e2e.test.ts b/end-to-end/test/payments.e2e.test.ts index 3ca5028c2..634285671 100644 --- a/end-to-end/test/payments.e2e.test.ts +++ b/end-to-end/test/payments.e2e.test.ts @@ -56,7 +56,7 @@ describeIntegration('Payments E2E', () => { const bobchain = await LocalchainWithSync.load({ localchainPath: Path.join(storageDir, 'bobchain.db'), mainchainUrl: argonMainchainUrl, - automaticallyRunSync: false, + disableAutomaticSync: true, channelHoldAllocationStrategy: { type: 'multiplier', queries: 2, @@ -134,10 +134,6 @@ describeIntegration('Payments E2E', () => { cloudAddress, { domain, - payment: { - notaryId: 1, - address: ferdie.address, - }, }, identityPath, ); @@ -229,10 +225,6 @@ describeIntegration('Payments E2E', () => { cloudAddress, { version: '0.0.2', - payment: { - notaryId: 1, - address: ferdie.address, - }, }, identityPath, ); diff --git a/specification/datastore/DatabrokerAdminApis.ts b/specification/datastore/DatabrokerAdminApis.ts index c9da14273..3e82a16f9 100644 --- a/specification/datastore/DatabrokerAdminApis.ts +++ b/specification/datastore/DatabrokerAdminApis.ts @@ -7,7 +7,7 @@ const OkSchema = z.object({ success: z.boolean() }); const organizationIdValidation = z .string() .length(21) - .regex(/[A-Za-z0-9_-]{21}/) + .regex(/^[A-Za-z0-9_-]{21}$/) .describe('The organization id'); const nameValidation = z.string().min(1).max(255).optional(); diff --git a/specification/datastore/DatastoreApis.ts b/specification/datastore/DatastoreApis.ts index 19a0fa489..148b42697 100644 --- a/specification/datastore/DatastoreApis.ts +++ b/specification/datastore/DatastoreApis.ts @@ -1,12 +1,12 @@ import { + identitySignatureValidation, identityValidation, microgonsValidation, - identitySignatureValidation, } from '@ulixee/platform-specification/types'; import { z } from 'zod'; import { DatastoreManifestWithLatest } from '../services/DatastoreRegistryApis'; import { datastoreIdValidation } from '../types/datastoreIdValidation'; -import { minDate } from '../types/IDatastoreManifest'; +import { DatastorePaymentRecipientSchema, minDate } from '../types/IDatastoreManifest'; import { DatastorePricing } from '../types/IDatastorePricing'; import { DatastoreStatsSchema } from '../types/IDatastoreStats'; import { PaymentSchema } from '../types/IPayment'; @@ -57,7 +57,7 @@ const DatastoreQueryMetadataSchema = z.object({ .optional(), affiliateId: z .string() - .regex(/aff[a-zA-Z_0-9-]{10}/) + .regex(/^aff[a-zA-Z_0-9-]{10}$/) .optional() .describe('A tracking id to attribute payments to source affiliates.'), payment: PaymentSchema.optional().describe('Payment for this request.'), @@ -88,7 +88,9 @@ export const DatastoreApiSchemas = { adminIdentity: identityValidation.describe( 'If this server is in production mode, an AdminIdentity approved on the Server or Datastore.', ), - adminSignature: identitySignatureValidation.describe('A signature from an approved AdminIdentity'), + adminSignature: identitySignatureValidation.describe( + 'A signature from an approved AdminIdentity', + ), }), result: z.object({ adminIdentity: identityValidation.describe('The admin identity who uploaded this Datastore.'), @@ -176,6 +178,7 @@ export const DatastoreApiSchemas = { .describe( 'A Typescript interface describing input and outputs of Datastore Extractors, and schemas of Datastore Tables', ), + payment: DatastorePaymentRecipientSchema.optional(), }), }, 'Datastore.stream': { diff --git a/specification/datastore/DomainLookupApis.ts b/specification/datastore/DomainLookupApis.ts index 526e07259..8853c771c 100644 --- a/specification/datastore/DomainLookupApis.ts +++ b/specification/datastore/DomainLookupApis.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { datastoreIdValidation } from '../types/datastoreIdValidation'; +import { DatastorePaymentRecipientSchema } from '../types/IDatastoreManifest'; import { semverValidation } from '../types/semverValidation'; import { IZodSchemaToApiTypes } from '../utils/IZodApi'; @@ -13,6 +14,7 @@ export const DomainLookupApiSchema = { version: semverValidation, host: z.string(), domain: z.string().optional(), + payment: DatastorePaymentRecipientSchema.optional(), }), }, }; diff --git a/specification/package.json b/specification/package.json index 23efb1166..6460286df 100644 --- a/specification/package.json +++ b/specification/package.json @@ -5,7 +5,8 @@ "main": "./index.js", "license": "MIT", "dependencies": { - "zod": "^3.22.4" + "zod": "^3.22.4", + "@argonprotocol/localchain": "0.0.8" }, "devDependencies": { "@ulixee/net": "2.0.0-alpha.29" diff --git a/specification/services/MicropaymentChannelApis.ts b/specification/services/MicropaymentChannelApis.ts index 62909eb58..7e1b2dc8e 100644 --- a/specification/services/MicropaymentChannelApis.ts +++ b/specification/services/MicropaymentChannelApis.ts @@ -2,11 +2,16 @@ import { z } from 'zod'; import { microgonsValidation } from '../types'; import { datastoreIdValidation } from '../types/datastoreIdValidation'; import { BalanceChangeSchema } from '../types/IBalanceChange'; +import { DatastorePaymentRecipientSchema } from '../types/IDatastoreManifest'; import { NoteSchema } from '../types/INote'; import { channelHoldIdValidation, PaymentSchema } from '../types/IPayment'; import { IZodHandlers, IZodSchemaToApiTypes } from '../utils/IZodApi'; export const MicropaymentChannelApiSchemas = { + 'MicropaymentChannel.getPaymentInfo': { + args: z.void(), + result: DatastorePaymentRecipientSchema, + }, 'MicropaymentChannel.importChannelHold': { args: z.object({ datastoreId: datastoreIdValidation, diff --git a/specification/types/IBalanceChange.ts b/specification/types/IBalanceChange.ts index 2832bf140..102c2aec2 100644 --- a/specification/types/IBalanceChange.ts +++ b/specification/types/IBalanceChange.ts @@ -1,12 +1,9 @@ import { z } from 'zod'; +import { AccountType } from '@argonprotocol/localchain'; import { addressValidation, milligonsValidation, multiSignatureValidation } from './index'; import { NoteSchema } from './INote'; -export enum AccountType { - Tax = 'tax', - Deposit = 'deposit', -} - +export { AccountType }; export const notaryIdValidation = z.number().int().positive(); export const tickValidation = z .number() diff --git a/specification/types/IDatastoreManifest.ts b/specification/types/IDatastoreManifest.ts index 7bed8a7ea..16414633e 100644 --- a/specification/types/IDatastoreManifest.ts +++ b/specification/types/IDatastoreManifest.ts @@ -1,14 +1,17 @@ import { addressValidation, identityValidation } from '@ulixee/platform-specification/types'; import { z } from 'zod'; +import { Chain } from '@argonprotocol/localchain'; import { datastoreIdValidation } from './datastoreIdValidation'; import { DatastorePricing } from './IDatastorePricing'; import { semverValidation } from './semverValidation'; export const minDate = new Date('2022-01-01').getTime(); export const DatastorePaymentRecipientSchema = z.object({ - address: addressValidation - .optional() - .describe('A payment address that indicates payments are required.'), + chain: z.nativeEnum(Chain), + genesisHash: z.string().regex(/^(0[xX])?[0-9a-fA-F]{64}$/), + address: addressValidation.describe( + 'A payment address microchannel payments should be made out to.', + ), notaryId: z.number(), }); @@ -29,7 +32,7 @@ export const DatastoreManifestSchema = z.object({ .describe('A sha256 of the script contents.') .length(62) .regex( - /^scr1[ac-hj-np-z02-9]{58}/, + /^scr1[ac-hj-np-z02-9]{58}$/, 'This is not a Datastore scriptHash (Bech32m encoded hash starting with "scr").', ), adminIdentities: identityValidation @@ -43,7 +46,7 @@ export const DatastoreManifestSchema = z.object({ extractorsByName: z.record( z .string() - .regex(/[a-z][A-Za-z0-9]+/) + .regex(/^[a-z][A-Za-z0-9]+$/) .describe('The Extractor name'), z.object({ description: z.string().optional(), @@ -71,7 +74,7 @@ export const DatastoreManifestSchema = z.object({ crawlersByName: z.record( z .string() - .regex(/[a-z][A-Za-z0-9]+/) + .regex(/^[a-z][A-Za-z0-9]+$/) .describe('The Crawler name'), z.object({ description: z.string().optional(), @@ -99,7 +102,7 @@ export const DatastoreManifestSchema = z.object({ tablesByName: z.record( z .string() - .regex(/[a-z][A-Za-z0-9]+/) + .regex(/^[a-z][A-Za-z0-9]+$/) .describe('The Table name'), z.object({ description: z.string().optional(), @@ -113,7 +116,6 @@ export const DatastoreManifestSchema = z.object({ ), }), ), - payment: DatastorePaymentRecipientSchema.optional(), domain: z .string() .optional() diff --git a/specification/types/IPayment.ts b/specification/types/IPayment.ts index 8494ffc3a..b9f857ff9 100644 --- a/specification/types/IPayment.ts +++ b/specification/types/IPayment.ts @@ -8,7 +8,7 @@ export const channelHoldIdValidation = z .string() .length(62) .regex( - /^esc1[ac-hj-np-z02-9]{58}/, + /^esc1[ac-hj-np-z02-9]{58}$/, 'This is not a Ulixee identity (Bech32m encoded public key starting with "esc1").', ); @@ -40,7 +40,7 @@ export const PaymentSchema = PaymentMethodSchema.extend({ uuid: z .string() .length(21) - .regex(/[A-Za-z0-9_-]{21}/) + .regex(/^[A-Za-z0-9_-]{21}$/) .describe('A one time payment id.'), microgons: microgonsValidation, }); diff --git a/specification/types/datastoreIdValidation.ts b/specification/types/datastoreIdValidation.ts index cde333938..be4e6e9f1 100644 --- a/specification/types/datastoreIdValidation.ts +++ b/specification/types/datastoreIdValidation.ts @@ -7,6 +7,6 @@ export const datastoreIdValidation = z .min(2) .max(50) .regex( - new RegExp(`^${datastoreRegex.source}`), - 'This is not a valid datastoreId (2-20 alphanumeric characters).', + new RegExp(`^${datastoreRegex.source}$`), + 'This is not a valid datastoreId (2-20 lowercase alphanumeric characters).', ); diff --git a/specification/types/index.ts b/specification/types/index.ts index 107681512..91f944ab4 100644 --- a/specification/types/index.ts +++ b/specification/types/index.ts @@ -3,13 +3,13 @@ import { z } from 'zod'; export const addressValidation = z .string() .length(48) - .regex(/[1-9A-HJ-NP-Za-km-z]+/); // base 58 + .regex(/^[1-9A-HJ-NP-Za-km-z]+$/); // base 58 export const identityValidation = z .string() .length(61) .regex( - /^id1[ac-hj-np-z02-9]{58}/, + /^id1[ac-hj-np-z02-9]{58}$/, 'This is not a Ulixee identity (Bech32m encoded public key starting with "id1").', ); diff --git a/specification/types/semverValidation.ts b/specification/types/semverValidation.ts index f1cdaa681..63da5cfe5 100644 --- a/specification/types/semverValidation.ts +++ b/specification/types/semverValidation.ts @@ -1,4 +1,7 @@ import { z } from 'zod'; export const semverRegex = /(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)/; -export const semverValidation = z.string().regex(semverRegex).describe('A semver'); +export const semverValidation = z + .string() + .regex(new RegExp(`^${semverRegex.source}$`)) + .describe('A semver'); diff --git a/yarn.lock b/yarn.lock index 91e531ef4..6e8ce66ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,46 +15,46 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@argonprotocol/localchain-darwin-arm64@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-darwin-arm64/-/localchain-darwin-arm64-0.0.6.tgz#c9f87e21488d463dd9d54d15ae134e3ee95cda5e" - integrity sha512-8fBG6yny/wniF757olt2JToIRAtGhhv7j9MBm4N03pzzKSWHUdgJ6Z8Cqh7g1oatwSSDCYv04SyW604oG81AMw== +"@argonprotocol/localchain-darwin-arm64@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-darwin-arm64/-/localchain-darwin-arm64-0.0.8.tgz#f7584d7cedae0a191436ad0ef630fb688976f4ee" + integrity sha512-741rNJo+v6Po5sOTVJVSeaVDyjKKD2ZKV9kAZxOjzYjSWDrBSf8e670GShP18RlxTr19KrRQsXSBq6hDDlRBLQ== -"@argonprotocol/localchain-darwin-x64@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-darwin-x64/-/localchain-darwin-x64-0.0.6.tgz#9ca8140bfa0d5a721637869d4feb2517c38a8af4" - integrity sha512-u2PB+Ubi5yvHQrdkjpVD8ITjsQtlqTtdE9Iy00Xg1nz44nwDJVkwSTYJQs/DYZt1cBIiOdQcmGI0eyBh+3eUJg== +"@argonprotocol/localchain-darwin-x64@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-darwin-x64/-/localchain-darwin-x64-0.0.8.tgz#7e7f2c22f27010a0c31eb5200902a3b39067d8d0" + integrity sha512-+dyJwUHtL4CZxz7RL7zenWSu9Z6niHn8M06wObLxgWmpwMZzgivlvn8HsPxCyeOV7PBIM375EIPYlX8UZhF34A== -"@argonprotocol/localchain-linux-arm64-gnu@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-linux-arm64-gnu/-/localchain-linux-arm64-gnu-0.0.6.tgz#49dbad6e67019bf14b60d0f3ba880d2a1f23e705" - integrity sha512-bIXyL3l8JblWuKoB366+Skkljrh9nIwMeBZ9ueLQkNAkhWSUCVfVylXD2pWmIjCuhe3hevsh6VIqsxednvWPVw== +"@argonprotocol/localchain-linux-arm64-gnu@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-linux-arm64-gnu/-/localchain-linux-arm64-gnu-0.0.8.tgz#7d78c16d9fb1591accfbbee86037f5a63aa9018e" + integrity sha512-rsIN5YnQJemJz5B5PGa+jpiRLeBpLxZv7BiE2/zMknqi8V5cZKlEb0hDRAoPKcgpjdDCjsQpnh4qyUnvXAgSIQ== -"@argonprotocol/localchain-linux-x64-gnu@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-linux-x64-gnu/-/localchain-linux-x64-gnu-0.0.6.tgz#2d68922a9c56d73ca05fd352b73fa804b98b2f42" - integrity sha512-RhQhepQabeIZm26OJBFhRx9Xdt0vzMGbalAt7Cf/VIhzT2B7ixulU2e54/TZFJxJs+4k292p7NLWHY4CajwvsA== +"@argonprotocol/localchain-linux-x64-gnu@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-linux-x64-gnu/-/localchain-linux-x64-gnu-0.0.8.tgz#005697d36f51c36c086c32cfb3103e7e9d11081d" + integrity sha512-Kx5M5GHOxL4P8PHnIZefqf1OYChE1r9Tl8Ce9G48KIEXKpzIaPqFyn/ATZd6tRzk0+7N0BRBFC1Y9Nkh9YlVWA== -"@argonprotocol/localchain-win32-x64-msvc@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-win32-x64-msvc/-/localchain-win32-x64-msvc-0.0.6.tgz#213100ac68bb0cafe3b69954eaa4c2df55cc601f" - integrity sha512-HB/soTGwFy2LisPJVZmnZolhq/w/wTzruelJfR0NiSA1V/rWtdDf3paAg+fi6pbSnl8FjD/l4Nu8LAcF1e6PAw== +"@argonprotocol/localchain-win32-x64-msvc@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@argonprotocol/localchain-win32-x64-msvc/-/localchain-win32-x64-msvc-0.0.8.tgz#22255ace414c06a65450918b9335d189b5df85d4" + integrity sha512-pSXW3eP0/kFsGvpMuSlZ6vZ72AfgjtTU61zcbSBcPPOdyQatD4GEWq+7uC+5fkz86Pv0/eCSC8iU4c21/V+VRw== -"@argonprotocol/localchain@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@argonprotocol/localchain/-/localchain-0.0.6.tgz#1b5079da4f0541120de8579e48ac9264dc1c182a" - integrity sha512-8wmIbI6TzXGr3ZaUPeOVjXM09SUAM4JstYWn7JNcpw64Vbf26IIiCpngpM9S6vnK7qJr9kGeFPyBYBraHjsdgA== +"@argonprotocol/localchain@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@argonprotocol/localchain/-/localchain-0.0.8.tgz#50edcb44b800fec3ccbcf6934ebb923ed4ab4ef5" + integrity sha512-HvOIxlH1nkhgzrIRNQkGvL0R8QuG3Nul2smppQrP9M6dbx7ZaZUQ2vccJfnGvjA6XrEFE37y5F4NAMQCQYoUtw== optionalDependencies: - "@argonprotocol/localchain-darwin-arm64" "0.0.6" - "@argonprotocol/localchain-darwin-x64" "0.0.6" - "@argonprotocol/localchain-linux-arm64-gnu" "0.0.6" - "@argonprotocol/localchain-linux-x64-gnu" "0.0.6" - "@argonprotocol/localchain-win32-x64-msvc" "0.0.6" + "@argonprotocol/localchain-darwin-arm64" "0.0.8" + "@argonprotocol/localchain-darwin-x64" "0.0.8" + "@argonprotocol/localchain-linux-arm64-gnu" "0.0.8" + "@argonprotocol/localchain-linux-x64-gnu" "0.0.8" + "@argonprotocol/localchain-win32-x64-msvc" "0.0.8" -"@argonprotocol/mainchain@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@argonprotocol/mainchain/-/mainchain-0.0.6.tgz#25896a6320fe39b231e7d8af8a160296b09524d5" - integrity sha512-ylyBj7JRdYRofy1t8sRfq3c6Wd8UFKr2VUTG41jxL6XaQ4c18Tiexj7qJaolOa9TipEIcFHb4vc4edCsZt9+tw== +"@argonprotocol/mainchain@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@argonprotocol/mainchain/-/mainchain-0.0.8.tgz#afe37b850b30f9ce76e07816c73fc89b77048e70" + integrity sha512-YCM7iTfMqt01BSnqfaoqimRMR11Eu987NF4qWGzgu7fCP09tnvqOB7ZIizT4BJMZnNi/8G6osQayDWpQ5S1aFg== dependencies: "@polkadot/api" "^12.2.3" tslib "^2.6.2" @@ -2693,10 +2693,10 @@ dependencies: "@ulixee/js-path" "^2.0.0-alpha.18" -"@ulixee/chrome-124-0@^6367.208.10": - version "6367.208.11" - resolved "https://registry.yarnpkg.com/@ulixee/chrome-124-0/-/chrome-124-0-6367.208.11.tgz#6e59358498b487f4a3cf36c18f39e4a872bfaff3" - integrity sha512-T36iEE0RVX+yWn5nWtjc7dcMEmxThhdRUG1tmqKHuhKfEH4Bh5jhezqlL8dBoAZvFQjdxkR4kIC0N3aR4qgS5A== +"@ulixee/chrome-128-0@^6613.138.11": + version "6613.138.11" + resolved "https://registry.yarnpkg.com/@ulixee/chrome-128-0/-/chrome-128-0-6613.138.11.tgz#9836f79a6f2ae4babe06f6fb34a706dfb632584c" + integrity sha512-efglwyxEq0UW/Qr6lvMi25aDwv8Ea4XwrLmd/MFPKAThZXI+vg6NoirT7lbtJ0RF02G6dvVX1OoEXnZH8rfQjg== dependencies: "@ulixee/chrome-app" "^1.0.3"