From 010115e98345842efa22dee771557ffc3588abba Mon Sep 17 00:00:00 2001 From: tahpot Date: Tue, 26 Dec 2023 07:14:22 +1030 Subject: [PATCH] Mainnet release (#355) --- packages/account-node/CHANGELOG.md | 7 + packages/account-node/package.json | 14 +- packages/account-node/src/auto.ts | 12 +- packages/account-node/src/nodeSelector.ts | 5 +- .../account-node/test/nodeSelector.test.ts | 4 +- packages/account-web-vault/CHANGELOG.md | 5 + packages/account-web-vault/package.json | 10 +- packages/account-web-vault/src/auth-client.ts | 4 +- .../account-web-vault/src/vault-account.ts | 7 +- .../src/vault-modal-login.ts | 9 +- packages/account/CHANGELOG.md | 5 + packages/account/package.json | 8 +- packages/account/src/account.ts | 2 +- packages/client-ts/CHANGELOG.md | 14 + packages/client-ts/package.json | 20 +- packages/client-ts/src/client.ts | 169 ++++++++++- packages/client-ts/src/context/context.ts | 36 ++- .../client-ts/src/context/engines/base.ts | 2 +- .../engines/verida/database/base-db.ts | 6 + .../context/engines/verida/database/client.ts | 2 - .../engines/verida/database/db-encrypted.ts | 141 ++++++--- .../engines/verida/database/endpoint.ts | 3 +- .../context/engines/verida/database/engine.ts | 84 +++++- .../engines/verida/database/interfaces.ts | 1 + .../context/engines/verida/messaging/inbox.ts | 1 + .../engines/verida/messaging/outbox.ts | 4 + .../client-ts/src/context/profiles/profile.ts | 10 + packages/client-ts/src/context/utils.ts | 9 +- packages/client-ts/src/index.ts | 5 +- packages/client-ts/src/utils/migration.ts | 107 +++++++ .../test/client.contexthash.tests.ts | 59 ++++ .../client-ts/test/client.destroy.tests.ts | 118 ++++++++ packages/client-ts/test/context.hash.tests.ts | 151 ++++++++++ packages/client-ts/test/migration.tests.ts | 259 +++++++++++++++++ .../client-ts/test/storage.failure.tests.ts | 169 +++++++++++ packages/did-client/CHANGELOG.md | 6 + packages/did-client/package.json | 12 +- packages/did-client/src/did-client.ts | 24 +- packages/did-document/package.json | 8 +- packages/helpers/CHANGELOG.md | 5 + packages/helpers/package.json | 7 +- packages/helpers/src/index.ts | 3 +- packages/helpers/src/verification.ts | 40 +++ packages/helpers/test/verification.test.ts | 32 ++ packages/keyring/package.json | 6 +- packages/storage-link/package.json | 10 +- packages/types/CHANGELOG.md | 7 + packages/types/src/ContextInterfaces.ts | 5 + packages/types/src/IDatabase.ts | 4 +- packages/types/src/IDatastore.ts | 10 +- packages/types/src/IStorageEngine.ts | 2 +- packages/types/src/IStorageNode.ts | 10 + packages/types/src/VdaClientConfig.ts | 17 ++ packages/types/src/Web3Interfaces.ts | 22 +- packages/types/src/index.ts | 4 +- packages/vda-common-test/src/const.ts | 3 +- packages/vda-common/CHANGELOG.md | 5 + packages/vda-common/package.json | 2 +- packages/vda-common/src/abi/SoulboundNFT.json | 19 ++ .../vda-common/src/abi/VDARewardContract.json | 19 ++ .../vda-common/src/abi/VeridaDIDLinkage.json | 19 ++ packages/vda-common/src/abi/VeridaToken.json | 32 +- packages/vda-common/src/contract.ts | 28 +- packages/vda-common/src/defaults.ts | 24 ++ packages/vda-common/src/index.ts | 3 +- packages/vda-common/src/rpc.ts | 4 +- packages/vda-common/src/utils.ts | 4 +- packages/vda-did-resolver/package.json | 12 +- packages/vda-did/.env.example | 5 +- packages/vda-did/CHANGELOG.md | 10 + packages/vda-did/package.json | 12 +- .../vda-did/src/blockchain/blockchainApi.ts | 32 +- packages/vda-did/src/blockchain/helpers.ts | 4 +- packages/vda-did/src/vdaDid.ts | 6 +- .../test/blockchain-api-mainnet-web3.test.ts | 84 ++++++ .../blockchain-api-testnet-gasless.test.ts | 179 ++++++++++++ ...kchain-api-testnet-web3-gas-config.test.ts | 273 ++++++++++++++++++ ...ts => blockchain-api-testnet-web3.test.ts} | 23 +- packages/vda-name-client/package.json | 8 +- .../src/blockchain/blockchainApi.ts | 27 +- packages/vda-sbt-client/CHANGELOG.md | 5 + packages/vda-sbt-client/package.json | 12 +- .../src/blockchain/blockchainApi.ts | 38 +-- .../test/blockchain-api.test.ts | 9 +- packages/vda-web3-client/CHANGELOG.md | 7 + packages/vda-web3-client/package.json | 6 +- .../vda-web3-client/src/VeridaContractBase.ts | 84 ++++-- packages/vda-web3-client/src/config.ts | 5 +- packages/vda-web3-client/src/utils.ts | 88 +++++- packages/verifiable-credentials/package.json | 12 +- packages/web-helpers/CHANGELOG.md | 5 + packages/web-helpers/package.json | 8 +- packages/web-helpers/src/WebUser.ts | 7 + yarn.lock | 9 + 94 files changed, 2499 insertions(+), 329 deletions(-) create mode 100644 packages/client-ts/src/utils/migration.ts create mode 100644 packages/client-ts/test/client.contexthash.tests.ts create mode 100644 packages/client-ts/test/client.destroy.tests.ts create mode 100644 packages/client-ts/test/context.hash.tests.ts create mode 100644 packages/client-ts/test/migration.tests.ts create mode 100644 packages/client-ts/test/storage.failure.tests.ts create mode 100644 packages/helpers/src/verification.ts create mode 100644 packages/helpers/test/verification.test.ts create mode 100644 packages/types/src/IStorageNode.ts create mode 100644 packages/types/src/VdaClientConfig.ts create mode 100644 packages/vda-common/src/defaults.ts create mode 100644 packages/vda-did/test/blockchain-api-mainnet-web3.test.ts create mode 100644 packages/vda-did/test/blockchain-api-testnet-gasless.test.ts create mode 100644 packages/vda-did/test/blockchain-api-testnet-web3-gas-config.test.ts rename packages/vda-did/test/{blockchain-api.test.ts => blockchain-api-testnet-web3.test.ts} (90%) diff --git a/packages/account-node/CHANGELOG.md b/packages/account-node/CHANGELOG.md index 78c7a4fb..b64dfa78 100644 --- a/packages/account-node/CHANGELOG.md +++ b/packages/account-node/CHANGELOG.md @@ -1,3 +1,10 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Fix: In some instances, duplicate storage nodes are selected +- Fix: Missing await on multiple `this.ensureAuthenticated()` calls +- Fix: Make `getDidClient()` async + 2023-05-12 (v.2.3.5) ------------------- diff --git a/packages/account-node/package.json b/packages/account-node/package.json index f6ebd0b0..00444030 100644 --- a/packages/account-node/package.json +++ b/packages/account-node/package.json @@ -1,6 +1,6 @@ { "name": "@verida/account-node", - "version": "2.3.9", + "version": "2.4.0-rc6", "main": "dist/index.js", "license": "ISC", "directories": { @@ -17,12 +17,12 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/account": "^2.3.9", - "@verida/did-client": "^2.3.9", - "@verida/did-document": "^2.3.3", - "@verida/encryption-utils": "^2.2.3", - "@verida/keyring": "^2.3.3", - "@verida/types": "^2.3.1", + "@verida/account": "^2.4.0-rc6", + "@verida/did-client": "^2.4.0-rc6", + "@verida/did-document": "^2.4.0-rc1", + "@verida/encryption-utils": "^2.2.2", + "@verida/keyring": "^2.4.0-rc1", + "@verida/types": "^2.4.0-rc1", "axios": "^0.27.2", "did-resolver": "^4.0.1" }, diff --git a/packages/account-node/src/auto.ts b/packages/account-node/src/auto.ts index f61ea06d..3f675437 100644 --- a/packages/account-node/src/auto.ts +++ b/packages/account-node/src/auto.ts @@ -99,7 +99,7 @@ export default class AutoAccount extends Account { } public async storageConfig(contextName: string, forceCreate?: boolean): Promise { - this.ensureAuthenticated() + await this.ensureAuthenticated() const did = await this.did() let storageConfig = await StorageLink.getLink(this.didClient, did, contextName, true) @@ -136,7 +136,7 @@ export default class AutoAccount extends Account { * @param storageConfig */ public async linkStorage(storageConfig: SecureContextConfig): Promise { - this.ensureAuthenticated() + await this.ensureAuthenticated() const keyring = await this.keyring(storageConfig.id) const result = await StorageLink.setLink(this.didClient, storageConfig, keyring, this.wallet.privateKey) @@ -156,7 +156,7 @@ export default class AutoAccount extends Account { * @param contextName */ public async unlinkStorage(contextName: string): Promise { - this.ensureAuthenticated() + await this.ensureAuthenticated() let result = await StorageLink.unlink(this.didClient, contextName) if (!result) { return false @@ -178,7 +178,7 @@ export default class AutoAccount extends Account { * */ public async linkStorageContextService(contextName: string, endpointType: SecureContextEndpointType, serverType: string, endpointUris: string[]): Promise { - this.ensureAuthenticated() + await this.ensureAuthenticated() const result = await StorageLink.setContextService(this.didClient, contextName, endpointType, serverType, endpointUris) for (let i in result) { @@ -191,8 +191,8 @@ export default class AutoAccount extends Account { return true } - public getDidClient(): DIDClient { - this.ensureAuthenticated() + public async getDidClient(): Promise { + await this.ensureAuthenticated() return this.didClient } diff --git a/packages/account-node/src/nodeSelector.ts b/packages/account-node/src/nodeSelector.ts index 6d29f783..0c3905c4 100644 --- a/packages/account-node/src/nodeSelector.ts +++ b/packages/account-node/src/nodeSelector.ts @@ -92,6 +92,7 @@ export class NodeSelector { const regionNodes = await this.nodesByRegion() let possibleNodes: StorageNode[] + if (!regionNodes[region]) { // no region nodes, find global nodes possibleNodes = await this.loadStorageNodes() @@ -104,7 +105,7 @@ export class NodeSelector { if (selectedNodes.length < numNodes) { // Not enough region nodes, try to find global nodes const globalNodes = await this.loadStorageNodes() - const globalFoundNodes = await this.selectNodesFromList(globalNodes, selectedNodes, numNodes - selectedNodes.length) + const globalFoundNodes = await this.selectNodesFromList(globalNodes, selectedNodes.concat(ignoredNodes), numNodes - selectedNodes.length) return selectedNodes.concat(globalFoundNodes) } @@ -118,6 +119,8 @@ export class NodeSelector { while (selectedNodes.length < numNodes && possibleNodes.length > 0) { const nodeIndex = getRandomInt(0, possibleNodes.length) const possibleNode = possibleNodes[nodeIndex] + + // check if the node is in the ignore list if (ignoredNodeIds.indexOf(possibleNode.id) !== -1) { possibleNodes.splice(nodeIndex, 1) continue diff --git a/packages/account-node/test/nodeSelector.test.ts b/packages/account-node/test/nodeSelector.test.ts index d7a4dcec..c41cb487 100644 --- a/packages/account-node/test/nodeSelector.test.ts +++ b/packages/account-node/test/nodeSelector.test.ts @@ -16,7 +16,7 @@ describe('Storage node selector tests', () => { defaultTimeout: 5000 }) - //nodeSelector.loadStorageNodes(TEST_NODES) + nodeSelector.loadStorageNodes(TEST_NODES) }) it('can avoid duplicates', async function() { @@ -24,7 +24,7 @@ describe('Storage node selector tests', () => { // the same node isn't included more than once let i = 0 while (i++ < 5) { - const nodes = await nodeSelector.selectNodesByCountry('IN', 3) + const nodes = await nodeSelector.selectNodesByCountry('AF', 3) const nodeIds = nodes.map((item) => item.id) const dedupeIds = nodeIds.filter((item, pos) => nodeIds.indexOf(item) == pos) assert.equal(nodeIds.length, dedupeIds.length, 'Duplicates found') diff --git a/packages/account-web-vault/CHANGELOG.md b/packages/account-web-vault/CHANGELOG.md index 24c9aab8..2bcf5e91 100644 --- a/packages/account-web-vault/CHANGELOG.md +++ b/packages/account-web-vault/CHANGELOG.md @@ -1,3 +1,8 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Fix: Don't inject modal if it is already in the DOM + 2023-04-20 (v2.3.0) ------------------- diff --git a/packages/account-web-vault/package.json b/packages/account-web-vault/package.json index 2281afd9..301dd4c1 100644 --- a/packages/account-web-vault/package.json +++ b/packages/account-web-vault/package.json @@ -1,6 +1,6 @@ { "name": "@verida/account-web-vault", - "version": "2.3.9", + "version": "2.4.0-rc6", "main": "dist/index.js", "license": "ISC", "directories": { @@ -15,10 +15,10 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/account": "^2.3.9", - "@verida/encryption-utils": "^2.2.3", - "@verida/keyring": "^2.3.3", - "@verida/types": "^2.3.1", + "@verida/account": "^2.4.0-rc6", + "@verida/encryption-utils": "^2.2.2", + "@verida/keyring": "^2.4.0-rc1", + "@verida/types": "^2.4.0-rc1", "jsonwebtoken": "^8.5.1", "lodash": "^4.17.21", "qrcode-with-logos": "^1.0.3", diff --git a/packages/account-web-vault/src/auth-client.ts b/packages/account-web-vault/src/auth-client.ts index 59827040..92aef80a 100644 --- a/packages/account-web-vault/src/auth-client.ts +++ b/packages/account-web-vault/src/auth-client.ts @@ -14,7 +14,9 @@ export default class AuthClient { schemeUri: 'veridavault://login-request', loginUri: 'https://vault.verida.io/request/', deeplinkId: 'verida-auth-client-deeplink', - request: {} + request: { + environment: config.environment + } }, config) this.modal = modal diff --git a/packages/account-web-vault/src/vault-account.ts b/packages/account-web-vault/src/vault-account.ts index 7e5d948d..49fd9be0 100644 --- a/packages/account-web-vault/src/vault-account.ts +++ b/packages/account-web-vault/src/vault-account.ts @@ -20,7 +20,10 @@ const CONFIG_DEFAULTS: Record = { loginUri: 'https://vault.verida.io/request', serverUri: `wss://auth.testnet.verida.io` }, - mainnet: {}, + mainnet: { + loginUri: 'https://vault.verida.io/request', + serverUri: `wss://auth.testnet.verida.io` + }, testnet: { loginUri: 'https://vault.verida.io/request', serverUri: `wss://auth.testnet.verida.io` @@ -85,7 +88,7 @@ export class VaultAccount extends Account { this.config.request.userAgent = navigator.userAgent if (!this.config.environment) { - this.config.environment = EnvironmentType.TESTNET + this.config.environment = EnvironmentType.MAINNET } this.config = _.merge(CONFIG_DEFAULTS[this.config.environment], this.config) diff --git a/packages/account-web-vault/src/vault-modal-login.ts b/packages/account-web-vault/src/vault-modal-login.ts index 8e1437ee..5af7559d 100644 --- a/packages/account-web-vault/src/vault-modal-login.ts +++ b/packages/account-web-vault/src/vault-modal-login.ts @@ -313,9 +313,12 @@ export default async function ( `; - document.body.insertAdjacentHTML("beforeend", modalHTML); + let modal: HTMLElement | null = document.getElementById("verida-modal"); + if (!modal) { + document.body.insertAdjacentHTML("beforeend", modalHTML); + modal = document.getElementById("verida-modal"); + } - const modal: HTMLElement | null = document.getElementById("verida-modal"); const closeModal: HTMLElement | null = document.getElementById("verida-modal-close"); @@ -335,7 +338,7 @@ export default async function ( if (modal && closeModal) { closeModal.onclick = () => { - modal.style.display = 'none'; + modal!.style.display = 'none'; authConfig.callbackRejected!(); } } diff --git a/packages/account/CHANGELOG.md b/packages/account/CHANGELOG.md index 5990fff5..e65ad243 100644 --- a/packages/account/CHANGELOG.md +++ b/packages/account/CHANGELOG.md @@ -1,3 +1,8 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Fix: Account disconnect should be optional, not throw an exception + 2023-04-20 (v2.3.0) ------------------- diff --git a/packages/account/package.json b/packages/account/package.json index cd4f7f19..5d52cafb 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -1,6 +1,6 @@ { "name": "@verida/account", - "version": "2.3.9", + "version": "2.4.0-rc6", "main": "dist/index.js", "license": "ISC", "directories": { @@ -17,9 +17,9 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/keyring": "^2.3.3", - "@verida/storage-link": "^2.3.9", - "@verida/types": "^2.3.1", + "@verida/keyring": "^2.4.0-rc1", + "@verida/storage-link": "^2.4.0-rc6", + "@verida/types": "^2.4.0-rc1", "did-jwt": "^6.11.0", "did-resolver": "^4.0.1", "lodash": "^4.17.21", diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index eaad727a..05b7e2bb 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -100,7 +100,7 @@ export default class Account implements IAccount { * For example, in a web browser context, it would remove any stored signatures from local storage. */ public async disconnect(contextName?: string): Promise { - throw new Error("Not implemented.") + return } public async getAuthContext(contextName: string, contextConfig: SecureContextConfig, authConfig: AuthTypeConfig = { diff --git a/packages/client-ts/CHANGELOG.md b/packages/client-ts/CHANGELOG.md index f0db624e..7bd223e9 100644 --- a/packages/client-ts/CHANGELOG.md +++ b/packages/client-ts/CHANGELOG.md @@ -1,3 +1,17 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Fix: Close outbox database connection after sending a message +- Feature: Expose new `migrateContext()` and `migrateDatase()` utility methods +- Feature: Implement utility method `context.deleteDatabase(databaseName)` +- Fix: Close databases when they are removed from the cache +- Fix: Close databases prior to deletion +- Feature: Support `verifyEncryptionKey: boolean` property on the config options when opening a database +- Fix: Closing encrypted database could have a race condition if called more than once +- Support `verifyWebsite()` helper method on `Profile` object (uses `.well-known/did.json`) +- Feature: Support `client.destroyContext()` +- Feature: Support `client.destroyAccount()` + 2023-05-12 (v.2.3.5) ------------------- diff --git a/packages/client-ts/package.json b/packages/client-ts/package.json index 49f51bbf..1632d657 100644 --- a/packages/client-ts/package.json +++ b/packages/client-ts/package.json @@ -1,6 +1,6 @@ { "name": "@verida/client-ts", - "version": "2.3.9", + "version": "2.4.0-rc6.1", "main": "dist/index.js", "license": "ISC", "directories": { @@ -17,14 +17,14 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/account": "^2.3.9", - "@verida/did-client": "^2.3.9", - "@verida/did-document": "^2.3.3", - "@verida/encryption-utils": "^2.2.3", - "@verida/keyring": "^2.3.3", - "@verida/storage-link": "^2.3.9", - "@verida/types": "^2.3.1", - "@verida/vda-name-client": "^2.3.6", + "@verida/account": "^2.4.0-rc6", + "@verida/did-client": "^2.4.0-rc6", + "@verida/did-document": "^2.4.0-rc1", + "@verida/encryption-utils": "^2.2.2", + "@verida/keyring": "^2.4.0-rc1", + "@verida/storage-link": "^2.4.0-rc6", + "@verida/types": "^2.4.0-rc1", + "@verida/vda-name-client": "^2.4.0-rc5", "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "axios": "^0.21.2", @@ -44,7 +44,7 @@ "@types/mocha": "^9.1.1", "@types/pouchdb": "^6.4.0", "@types/uuid": "^8.3.4", - "@verida/account-node": "^2.3.9", + "@verida/account-node": "^2.4.0-rc6", "dependency-cruiser": "^11.3.0", "mocha": "^8.2.1", "ts-mocha": "^8.0.0", diff --git a/packages/client-ts/src/client.ts b/packages/client-ts/src/client.ts index 1cc2f713..6d5d2578 100644 --- a/packages/client-ts/src/client.ts +++ b/packages/client-ts/src/client.ts @@ -1,4 +1,4 @@ -import { IProfile, IClient, ClientConfig, DefaultClientConfig, IAccount, IContext, EnvironmentType, SecureContextConfig } from "@verida/types"; +import { IProfile, IClient, ClientConfig, DefaultClientConfig, IAccount, IContext, EnvironmentType, SecureContextConfig, SecureContextEndpointType } from "@verida/types"; import { DIDClient } from "@verida/did-client"; import { VeridaNameClient } from '@verida/vda-name-client' @@ -6,6 +6,9 @@ import Context from "./context/context"; import DIDContextManager from "./did-context-manager"; import Schema from "./context/schema"; import DEFAULT_CONFIG from "./config"; +import Axios from "axios"; +import { ServiceEndpoint } from "did-resolver"; +import { DIDDocument } from "@verida/did-document"; const _ = require("lodash"); /** @@ -270,8 +273,9 @@ class Client implements IClient { delete _data["_rev"]; let validSignatures = []; - for (let key in data.signatures) { - const signerParts = key.match(/did:vda:([^]*):([^]*)\?context=(.*)$/); + for (let sigIndex in data.signatures) { + const signature = data.signatures[sigIndex] + const signerParts = sigIndex.match(/did:vda:([^]*):([^]*)\?context=(.*)$/); if (!signerParts || signerParts.length != 4) { continue; } @@ -283,7 +287,6 @@ class Client implements IClient { const signerDid = `did:vda:${sNetwork}:${sDid}`; if (!did || signerDid.toLowerCase() == did.toLowerCase()) { - const signature = data.signatures[key]; const didDocument = await this.didClient.get(signerDid); if (!didDocument) { continue; @@ -292,7 +295,7 @@ class Client implements IClient { const validSig = didDocument.verifyContextSignature( _data, sContext, - signature, + signature['secp256k1'], true ); @@ -305,6 +308,162 @@ class Client implements IClient { return validSignatures; } + public async destroyAccount() { + // Check user authenticated + if (!this.account) { + throw new Error('Account must be connected to get context name from hash') + } + + // @ts-ignore + if (!this.account.getDidClient) { + throw new Error('Account object is not capable of deleting DID') + } + + // Get the DID document of this user + // @ts-ignore + const didDocument = await this.didClient.get(this.did!) + const doc = didDocument.export() + + // Find all contexts for this account + const contextNames = [] + for (let i in doc.keyAgreement) { + // @ts-ignore + const keyAgreement = doc.keyAgreement[i] + const matches = keyAgreement.match(/=(0x[^&]*)/) + + if (matches.length == 2) { + const contextHash = matches[1] + try { + const contextName = await this.getContextNameFromHash(contextHash, didDocument) + contextNames.push(contextName) + } catch (err) { + // skip if the context hash doesn't exist + } + } + } + + // Destroy all the contexts + for (let c in contextNames) { + await this.destroyContext(contextNames[c]) + } + + // Destroy the DID on the blockchain + // @ts-ignore + const didClient = await this.account!.getDIDClient() + await didClient.destroy() + + // Logout the account + this.account = undefined + this.did = undefined + this.didContextManager = new DIDContextManager(this.didClient); + } + + public async destroyContext(contextName: string) { + // Check user authenticated + if (!this.account) { + throw new Error('Account must be connected to get context name from hash') + } + + const timestamp = parseInt(((new Date()).getTime() / 1000.0).toString()) + const did = (await this.account!.did()).toLowerCase() + + // Locate endpoints for the contextName + const didDocument = await this.didClient.get(this.did!) + // @ts-ignore + const endpointInfo = didDocument.locateServiceEndpoint(contextName, SecureContextEndpointType.DATABASE) + if (!endpointInfo) { + throw new Error('Context not found in DID Document') + } + + const endpointUris: ServiceEndpoint[] = endpointInfo!.serviceEndpoint + + // Delete context from all endpoints + // For each endpoint; this deletes all context databases, plus the database that tracks all databases for a context + const promises = [] + for (let e in endpointUris) { + let endpointUri = endpointUris[e] + endpointUri = endpointUri.substring(0, endpointUri.length-1) // strip trailing slash + const consentMessage = `Delete context (${contextName}) from server: "${endpointUri}"?\n\n${did}\n${timestamp}` + const signature = await this.account!.sign(consentMessage) + + promises.push(Axios.post(`${endpointUri}/user/destroyContext`, { + did, + timestamp, + signature, + contextName + })); + } + + const results = await Promise.allSettled(promises) + let resultIndex = 0 + let failureCount = 0 + const promiseResults: any = {} + for (let e in endpointUris) { + const endpoint = endpointUris[e].toString() + const result = results[resultIndex++] + promiseResults[endpoint] = result + + if (result.status !== 'fulfilled') { + failureCount++ + } + } + + // Remove the context from the DID document + await this.account!.unlinkStorage(contextName) + + return promiseResults + } + + public async getContextNameFromHash(contextHash: string, didDocument?: DIDDocument) { + // Check user authenticated + if (!this.account) { + throw new Error('Account must be connected to get context name from hash') + } + + // Get the DID document of this user + if (!didDocument) { + // @ts-ignore + didDocument = await this.didClient.get(this.did!) + } + const services = didDocument.export().service! + + // Locate the endpoints for the given context hash + const service = services.find((item) => item.id.match(contextHash) && item.type == 'VeridaDatabase') + if (!service) { + throw new Error(`Unable to locate service associated with context hash ${contextHash}`) + } + + const timestamp = parseInt(((new Date()).getTime() / 1000.0).toString()) + const did = (await this.account!.did()).toLowerCase() + + // Loop through endpoints, hitting `/user/contextHash` until a response is received + const endpoints: ServiceEndpoint[] = service.serviceEndpoint + for (let e in endpoints) { + let endpointUri = endpoints[e] + endpointUri = endpointUri.substring(0, endpointUri.length-1) // strip trailing slash + + const consentMessage = `Obtain context hash (${contextHash}) for server: "${endpointUri}"?\n\n${did}\n${timestamp}` + const signature = await this.account!.sign(consentMessage) + + try { + const response = await Axios.post(`${endpointUri}/user/contextHash`, { + did, + timestamp, + signature, + contextHash + }); + + if (response.data.status == 'success') { + return response.data.result.contextName + } + } catch (err) { + // ignore errors, try another endpoint + } + } + + throw new Error(`Unable to access any endpoints associated with context hash ${contextHash}`) + } + /** * Get a Schama instance by URL. * diff --git a/packages/client-ts/src/context/context.ts b/packages/client-ts/src/context/context.ts index 492bd99d..d097bd11 100644 --- a/packages/client-ts/src/context/context.ts +++ b/packages/client-ts/src/context/context.ts @@ -72,7 +72,7 @@ class Context extends EventEmitter implements IContext { private databaseEngines: DatabaseEngines = {}; private dbRegistry: DbRegistry; - private databaseCache: Record> = {} + private databaseCache: Record = {} /** * Instantiate a new context. @@ -331,7 +331,7 @@ class Context extends EventEmitter implements IContext { } const instance = this - this.databaseCache[cacheKey] = new Promise(async (resolve, rejects) => { + const promise = new Promise(async (resolve, rejects) => { //const now = (new Date()).getTime() try { const databaseEngine = await instance.getDatabaseEngine( @@ -356,6 +356,8 @@ class Context extends EventEmitter implements IContext { } }) + this.databaseCache[cacheKey] = await promise + return this.databaseCache[cacheKey] } @@ -426,6 +428,21 @@ class Context extends EventEmitter implements IContext { return database } + public async deleteDatabase(databaseName: string) { + if (!this.account) { + throw new Error(`Unable to delete database. No authenticated user.`); + } + + // Close the database if it's open + const accountDid = await this.account!.did() + const databaseEngine = await this.getDatabaseEngine( + accountDid, + false + ); + + return await databaseEngine.deleteDatabase(databaseName) + } + /** * Open a dataastore owned by this account. * @@ -547,13 +564,17 @@ class Context extends EventEmitter implements IContext { * Closes all open database connections, returns resources, cancels event listeners */ public async close(options: ContextCloseOptions = { - clearLocal: false + clearLocal: true }): Promise { // close all the other databases for (let d in this.databaseCache) { const database = await this.databaseCache[d] await database.close( options) } + + // The DbRegistry database has been closed. Reset to a clean instance so + // it will be re-opened if necessary + this.dbRegistry = new DbRegistry(this) } public async clearDatabaseCache(did: string, databaseName: string) { @@ -561,6 +582,15 @@ class Context extends EventEmitter implements IContext { for (let t in types) { const cacheKey = `${did.toLowerCase()}/${databaseName}/${types[t]}` if (this.databaseCache[cacheKey]) { + // try to close the database + try { + await this.databaseCache[cacheKey].close({ + clearLocal: true + }) + } catch (err: any) { + // already closed + } + delete this.databaseCache[cacheKey] } } diff --git a/packages/client-ts/src/context/engines/base.ts b/packages/client-ts/src/context/engines/base.ts index 7df171af..3c9d3909 100644 --- a/packages/client-ts/src/context/engines/base.ts +++ b/packages/client-ts/src/context/engines/base.ts @@ -73,7 +73,7 @@ class BaseStorageEngine extends EventEmitter { public async deleteDatabase( databaseName: string, - config: DatabaseDeleteConfig + config?: DatabaseDeleteConfig ): Promise { throw new Error("Not implemented"); } diff --git a/packages/client-ts/src/context/engines/verida/database/base-db.ts b/packages/client-ts/src/context/engines/verida/database/base-db.ts index adcfbaee..93af2067 100644 --- a/packages/client-ts/src/context/engines/verida/database/base-db.ts +++ b/packages/client-ts/src/context/engines/verida/database/base-db.ts @@ -302,6 +302,12 @@ class BaseDb extends EventEmitter implements IDatabase { this.db = await this.endpoint.connectDb(this.did, this.databaseName, this.permissions, this.isOwner!) } + // This is called when an endpoint is found to have died + public async replaceEndpoint() { + this.endpoint = await this.engine.getActiveEndpoint(true, true) + this.db = await this.endpoint.connectDb(this.did, this.databaseName, this.permissions, this.isOwner!) + } + /** * Update the users that can access the database */ diff --git a/packages/client-ts/src/context/engines/verida/database/client.ts b/packages/client-ts/src/context/engines/verida/database/client.ts index 98f879dd..99807596 100644 --- a/packages/client-ts/src/context/engines/verida/database/client.ts +++ b/packages/client-ts/src/context/engines/verida/database/client.ts @@ -150,8 +150,6 @@ export class DatastoreServerClient { await this.reAuth() return this.pingDatabases(databaseHashes, isWritePublic, did, contextName, false) } - //console.log(`error with pingDatabase() ${err.response.data.message}`) - // Ignore errors for now as the endpoint doesn't exist on storage nodes } } diff --git a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts index 92364339..8ae46f6d 100644 --- a/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts +++ b/packages/client-ts/src/context/engines/verida/database/db-encrypted.ts @@ -75,11 +75,14 @@ class EncryptedDatabase extends BaseDb { // Setting to 1,000 -- Any higher and it takes too long on mobile devices }); + await this.initSync() + } + + protected async initSync() { const databaseName = this.databaseName; /* @ts-ignore */ const instance = this; - //console.log(`Db.init-2(${databaseName}): ${(new Date()).getTime()-now}`) // Do a once off sync to ensure the local database pulls all data from remote server // before commencing live syncronisation between the two databases @@ -115,23 +118,25 @@ class EncryptedDatabase extends BaseDb { * If there is data in this database, it ensures the current encryption key * can decrypt the data. */ - try { - await this.getMany(); - //console.log(`Db.init-5(${databaseName}): ${(new Date()).getTime()-now}`) - } catch (err: any) { - // This error message is thrown by the underlying decrypt library if the - // data can't be decrypted - if ( - err.message == `Unsupported state or unable to authenticate data` || - err.message == "Could not decrypt!" - ) { - // Clear the instantiated PouchDb instances and throw a more useful exception - await this.close() - throw new Error(`Invalid encryption key supplied`); + if (this.config.verifyEncryptionKey) { + try { + await this.getMany(); + //console.log(`Db.init-5(${databaseName}): ${(new Date()).getTime()-now}`) + } catch (err: any) { + // This error message is thrown by the underlying decrypt library if the + // data can't be decrypted + if ( + err.message == `Unsupported state or unable to authenticate data` || + err.message == "Could not decrypt!" + ) { + // Clear the instantiated PouchDb instances and throw a more useful exception + await this.close() + throw new Error(`Invalid encryption key supplied`); + } + + // Unknown error, rethrow + throw err; } - - // Unknown error, rethrow - throw err; } //console.log(`Db.init-Final(${databaseName}): ${(new Date()).getTime()-now}`) @@ -149,6 +154,7 @@ class EncryptedDatabase extends BaseDb { if (this._sync) { // Cancel any existing sync this._sync.cancel(); + this._syncError = null } const instance = this; @@ -156,7 +162,8 @@ class EncryptedDatabase extends BaseDb { this._sync = PouchDB.sync(this._localDbEncrypted, this.db, { live: true, - retry: true, + retry: false, // don't retry on error, so we can quietly handle nodes going down + timeout: 5000, // Dont sync design docs filter: function (doc: any) { return doc._id.indexOf("_design") !== 0; @@ -178,18 +185,17 @@ class EncryptedDatabase extends BaseDb { instance._syncStatus = 'complete' instance._syncInfo = info }) - .on("error", function (err: any) { + .on("error", async function (err: any) { + instance._syncStatus = 'error' instance._syncError = err; - console.error( - `Unknown error occurred syncing with remote database: ${databaseName}` - ); - console.error(err); + + await instance.replaceEndpoint() }) .on("denied", function (err: any) { - console.error( - `Permission denied to sync with remote database: ${databaseName}` - ); - console.error(err); + instance._syncStatus = 'denied' + instance._syncError = err; + + instance.replaceEndpoint() }); return this._sync; @@ -228,18 +234,22 @@ class EncryptedDatabase extends BaseDb { public async close(options: DatabaseCloseOptions = { clearLocal: false }) { + if (this._sync === null) { + // No sync object indicates this database is closed + return + } + + await this.finalizeSync() + if (options.clearLocal) { await this.destroy({ localOnly: true }) + // Return, because destroy will close all database connections return } - if (this._sync) { - this._sync.cancel(); - } - try { await this._localDbEncrypted.close(); } catch (err) { @@ -252,17 +262,13 @@ class EncryptedDatabase extends BaseDb { // may already be closed } - this._sync = null; - this._syncError = null; await this.engine.closeDatabase(this.did, this.databaseName) this.emit('closed', this.databaseName) } - public async destroy(options: DatabaseDeleteConfig = { - localOnly: false - }): Promise { - if (!this.isOwner && !options.localOnly) { - throw new Error(`Unable to update users for a database you don't own`) + private async finalizeSync() { + if (!this._sync) { + return } // Need to ensure any database sync to remote server has completed @@ -276,12 +282,60 @@ class EncryptedDatabase extends BaseDb { instance._sync.on('complete', async () => { resolve() }) + instance._sync.on('error', async () => { + // If we have an error, that's okay, because the final replication will + // fix any issues or replace the endpoint if required + resolve() + }) }) + // console.log('waiting for sync to complete', this._syncStatus, this._sync.pull.state, this._sync.push.state) // Wait until sync completes await promise } + // Cancel the current sync + await this._sync.cancel() + this._sync = null + this._syncError = null + + // Perform one final replication to the remote server + try { + const result = await PouchDB.replicate(this._localDbEncrypted, this.db, { + live: false, // do a once off sync + retry: false, // don't retry, just fail + timeout: 5000, // 5 second timeout + }) + + // replication completed successfully + } catch (err) { + // Replication has failed, this is likely because the endpoint is down + // We need to connect to a different endpoint + await this.replaceEndpoint() + + // Try again + try { + const result = await PouchDB.replicate(this._localDbEncrypted, this.db, { + live: false, // do a once off sync + retry: false, // don't retry, just fail + timeout: 5000, // 5 second timeout + }) + } catch (err) { + console.log(err) + throw new Error(`Unable to sync data with network when closing database ${this.databaseName}`) + } + } + } + + public async destroy(options: DatabaseDeleteConfig = { + localOnly: false + }): Promise { + if (!this.isOwner && !options.localOnly) { + throw new Error(`Unable to update users for a database you don't own`) + } + + await this.finalizeSync() + // Actually perform database deletion await this._destroy(options) } @@ -290,20 +344,25 @@ class EncryptedDatabase extends BaseDb { localOnly: false }): Promise { try { - // Destory the local pouch database (this deletes this._local and this._localDbEncrypted as they share the same underlying data source) + // Destroy the local pouch database (this deletes this._local and this._localDbEncrypted as they share the same underlying data source) await this._localDbEncrypted.destroy() + } catch (err) { + // do nothing, database is likely already destroyed + } + try { if (!options.localOnly) { // Only delete remote database if required await this.engine.deleteDatabase(this.databaseName) } - + await this.close({ clearLocal: false }) } catch (err) { - console.error(err) + console.log(err) } + } public async updateUsers( diff --git a/packages/client-ts/src/context/engines/verida/database/endpoint.ts b/packages/client-ts/src/context/engines/verida/database/endpoint.ts index 1ac32285..109e15a0 100644 --- a/packages/client-ts/src/context/engines/verida/database/endpoint.ts +++ b/packages/client-ts/src/context/engines/verida/database/endpoint.ts @@ -98,8 +98,7 @@ export default class Endpoint extends EventEmitter { // Ping database to ensure replication is active // No need to await // Retry if auth error if we are the database owner - console.log('a') - instance.client.pingDatabases([databaseHash], isPublicWrite, did, instance.contextName, isOwner) + await instance.client.pingDatabases([databaseHash], isPublicWrite, did, instance.contextName, isOwner) if (result.status == 401) { throw new Error(`Permission denied to access server: ${instance.toString()}`) diff --git a/packages/client-ts/src/context/engines/verida/database/engine.ts b/packages/client-ts/src/context/engines/verida/database/engine.ts index 2fcf9895..8540d1c2 100644 --- a/packages/client-ts/src/context/engines/verida/database/engine.ts +++ b/packages/client-ts/src/context/engines/verida/database/engine.ts @@ -5,7 +5,7 @@ import PublicDatabase from "./db-public"; import DbRegistry from "../../../db-registry"; import Context from '../../../context'; import Endpoint from "./endpoint"; -import { ContextDatabaseInfo, DatabaseOpenConfig, DatabasePermissionsConfig, IDatabase, SecureContextConfig } from "@verida/types"; +import { ContextDatabaseInfo, DatabaseOpenConfig, DatabaseDeleteConfig, DatabasePermissionsConfig, IDatabase, SecureContextConfig } from "@verida/types"; const _ = require("lodash"); @@ -45,7 +45,7 @@ class StorageEngineVerida extends BaseStorageEngine { } } - private async locateAvailableEndpoint(endpoints: Record, checkStatus = true): Promise { + public async locateAvailableEndpoint(endpoints: Record, checkStatus = true): Promise { // Maintain a list of failed endpoints const failedEndpoints = [] @@ -72,7 +72,7 @@ class StorageEngineVerida extends BaseStorageEngine { try { const status = await endpoints[primaryEndpointUri].getStatus() if (status.data.status != 'success') { - throw new Error() + throw new Error('Storage node is not available') } return endpoints[primaryEndpointUri] @@ -82,6 +82,7 @@ class StorageEngineVerida extends BaseStorageEngine { failedEndpoints.push(primaryEndpointUri) primaryIndex++ primaryIndex = primaryIndex % Object.keys(endpoints).length + primaryEndpointUri = Object.keys(endpoints)[primaryIndex] } } @@ -91,7 +92,11 @@ class StorageEngineVerida extends BaseStorageEngine { /** * Get an active endpoint */ - public async getActiveEndpoint(checkStatus: boolean = true) { + public async getActiveEndpoint(checkStatus: boolean = true, clearActive: boolean = false) { + if (clearActive) { + this.activeEndpoint = undefined + } + if (this.activeEndpoint) { return this.activeEndpoint } @@ -174,6 +179,7 @@ class StorageEngineVerida extends BaseStorageEngine { }, did: this.accountDid, readOnly: false, + verifyEncryptionKey: true }, options ); @@ -302,6 +308,7 @@ class StorageEngineVerida extends BaseStorageEngine { endpoint, isOwner: config.isOwner, saveDatabase: config.saveDatabase, + verifyEncryptionKey: config.verifyEncryptionKey }, this ); @@ -368,6 +375,7 @@ class StorageEngineVerida extends BaseStorageEngine { endpoint, isOwner: config.isOwner, saveDatabase: config.saveDatabase, + verifyEncryptionKey: config.verifyEncryptionKey }, this ); @@ -428,9 +436,23 @@ class StorageEngineVerida extends BaseStorageEngine { promises.push(endpoint.createDb(databaseName, permissions, retry)) } - // No need for await as this can occur in the background? - const result = await Promise.all(promises) - //console.log(`createDb(${databaseName}, ${did}): ${(new Date()).getTime()-now}`) + // A node may be down, so quietly ignore that + // If all nodes are down, throw an error as they database won't have been created + const results = await Promise.allSettled(promises) + let resultIndex = 0 + let failureCount = 0 + for (let i in this.endpoints) { + const endpoint = this.endpoints[i] + const result = results[resultIndex++] + + if (result.status !== 'fulfilled') { + failureCount++ + } + } + + if (failureCount == Object.keys(this.endpoints).length) { + throw new Error(`Unable to create database (${databaseName}) on remote nodes`) + } // Call check replication to ensure this new database gets replicated across all nodes await this.checkReplication(databaseName) @@ -447,15 +469,31 @@ class StorageEngineVerida extends BaseStorageEngine { promises.push(endpoint.updateDatabase(databaseName, options)) } - // No need for await as this can occur in the background? - const result = await Promise.all(promises) - //console.log(`createDb(${databaseName}, ${did}): ${(new Date()).getTime()-now}`) + // A node may be down, so quietly ignore that + // If all nodes are down, throw an error as they database won't have been updated + const results = await Promise.allSettled(promises) + let resultIndex = 0 + let failureCount = 0 + for (let i in this.endpoints) { + const endpoint = this.endpoints[i] + const result = results[resultIndex++] + + if (result.status !== 'fulfilled') { + failureCount++ + } + } + + if (failureCount == Object.keys(this.endpoints).length) { + throw new Error(`Unable to update database (${databaseName}) on remote nodes`) + } } /** * Call deleteDatabase() on all the endpoints */ - public async deleteDatabase(databaseName: string): Promise { + public async deleteDatabase(databaseName: string, config?: DatabaseDeleteConfig): Promise { + await this.closeDatabase(this.accountDid!, databaseName) + //const now = (new Date()).getTime() const promises = [] for (let i in this.endpoints) { @@ -463,10 +501,26 @@ class StorageEngineVerida extends BaseStorageEngine { promises.push(endpoint.deleteDatabase(databaseName)) } - // delete from database registry + // Check the status of the storage node delete requests + // A node may be down, so quietly ignore that + // If all nodes are down, throw an error as they database won't have been deleted + const results = await Promise.allSettled(promises) + let resultIndex = 0 + let failureCount = 0 + for (let i in this.endpoints) { + const endpoint = this.endpoints[i] + const result = results[resultIndex++] - // No need for await as this can occur in the background? - const result = await Promise.all(promises) + if (result.status !== 'fulfilled') { + failureCount++ + } + } + + if (failureCount == Object.keys(this.endpoints).length) { + throw new Error(`Unable to delete database (${databaseName}) on remote nodes`) + } + + // delete from database registry const dbRegistry = this.context.getDbRegistry() await dbRegistry.removeDb(databaseName, this.accountDid!, this.storageContext) //console.log(`createDb(${databaseName}, ${did}): ${(new Date()).getTime()-now}`) @@ -508,8 +562,6 @@ class StorageEngineVerida extends BaseStorageEngine { for (let e in this.endpoints) { this.endpoints[e].disconnectDatabase(did, databaseName) } - - // @todo delete from registry } } diff --git a/packages/client-ts/src/context/engines/verida/database/interfaces.ts b/packages/client-ts/src/context/engines/verida/database/interfaces.ts index d4538b4e..143106a6 100644 --- a/packages/client-ts/src/context/engines/verida/database/interfaces.ts +++ b/packages/client-ts/src/context/engines/verida/database/interfaces.ts @@ -16,6 +16,7 @@ export interface VeridaDatabaseConfig { readOnly?: boolean; isOwner?: boolean; + verifyEncryptionKey?: boolean; encryptionKey?: Buffer; saveDatabase: boolean; diff --git a/packages/client-ts/src/context/engines/verida/messaging/inbox.ts b/packages/client-ts/src/context/engines/verida/messaging/inbox.ts index 4fc85e0f..8eaaaa59 100644 --- a/packages/client-ts/src/context/engines/verida/messaging/inbox.ts +++ b/packages/client-ts/src/context/engines/verida/messaging/inbox.ts @@ -99,6 +99,7 @@ class VeridaInbox extends EventEmitter { // We have a conflict. This can happen if `processItem()` is called twice // for the same inbox item. This can occur if called via the PouchDB changes // listener and also by the `processAll()` method call inside `init()`. + this.emit("newMessage", inboxEntry); return; } diff --git a/packages/client-ts/src/context/engines/verida/messaging/outbox.ts b/packages/client-ts/src/context/engines/verida/messaging/outbox.ts index 57c5debc..d68b221d 100644 --- a/packages/client-ts/src/context/engines/verida/messaging/outbox.ts +++ b/packages/client-ts/src/context/engines/verida/messaging/outbox.ts @@ -184,6 +184,10 @@ class VeridaOutbox { outboxEntry.sent = true; const outboxResponse = await outbox.save(outboxEntry); + // Close the database connection to the other user's inbox + // Don't do `await` as there's no need to slow things down + inbox.close() + return inboxResponse; } diff --git a/packages/client-ts/src/context/profiles/profile.ts b/packages/client-ts/src/context/profiles/profile.ts index 57456ac0..a43026c3 100644 --- a/packages/client-ts/src/context/profiles/profile.ts +++ b/packages/client-ts/src/context/profiles/profile.ts @@ -2,6 +2,7 @@ const EventEmitter = require("events"); import { DatabasePermissionOptionsEnum, IProfile } from "@verida/types"; import Context from "../context"; import Datastore from "../datastore"; +import { verifyDidControlsDomain } from '@verida/helpers' const _ = require("lodash"); interface ProfileDocument { @@ -159,6 +160,15 @@ export class Profile extends EventEmitter implements IProfile { await this.store!.changes(cb); } + public async verifyWebsite(): Promise { + const domain = await this.get('website') + if (!domain) { + return false + } + + return verifyDidControlsDomain(this.did, domain) + } + private async getRecord(): Promise { await this.init(); try { diff --git a/packages/client-ts/src/context/utils.ts b/packages/client-ts/src/context/utils.ts index 234e04ea..05129a6b 100644 --- a/packages/client-ts/src/context/utils.ts +++ b/packages/client-ts/src/context/utils.ts @@ -43,7 +43,14 @@ export class RecordSignature { _data['schema'] = Schema.getVersionlessSchemaName(_data['schema']) } - data.signatures[signKey.toLowerCase()] = await keyring.sign(_data); + const sig = await keyring.sign(_data) + + // Create empty signature object if this DID hasn't signed, or if this DID has an old signature format (string, not object) + if (!data.signatures[signKey.toLowerCase()] || typeof(data.signatures[signKey.toLowerCase()]) === 'string') { + data.signatures[signKey.toLowerCase()] = {} + } + + data.signatures[signKey.toLowerCase()]['secp256k1'] = sig; return data; } } diff --git a/packages/client-ts/src/index.ts b/packages/client-ts/src/index.ts index 505ef1b8..4d735f35 100644 --- a/packages/client-ts/src/index.ts +++ b/packages/client-ts/src/index.ts @@ -1,9 +1,12 @@ import Client from './client' import Network from './network' import Context from './context/context' +import { migrateContext, migrateDatabase } from './utils/migration' export { Client, Context, - Network + Network, + migrateContext, + migrateDatabase } \ No newline at end of file diff --git a/packages/client-ts/src/utils/migration.ts b/packages/client-ts/src/utils/migration.ts new file mode 100644 index 00000000..c492e4e3 --- /dev/null +++ b/packages/client-ts/src/utils/migration.ts @@ -0,0 +1,107 @@ +import { IContext, IDatabase } from "@verida/types"; +const _ = require("lodash"); +import { EventEmitter } from 'events' +import EncryptedDatabase from "../context/engines/verida/database/db-encrypted"; + +/** + * + * Note: May need the ability to force override the DID if migrating data between testnet -> mainnet? + * + * @param sourceContext + * @param destinationContext + */ +export function migrateContext(sourceContext: IContext, destinationContext: IContext): EventEmitter { + const eventManager = new EventEmitter() + _migrateContext(sourceContext, destinationContext, eventManager) + return eventManager +} + +async function _migrateContext(sourceContext: IContext, destinationContext: IContext, eventManager: EventEmitter) { + const sourceAccount = sourceContext.getAccount() + const sourceDid = await sourceAccount.did() + + const sourceDbEngine = await sourceContext.getDatabaseEngine(sourceDid) + const sourceDbInfo: any = await sourceDbEngine.info() + const sourceDatabases = sourceDbInfo.databases + + eventManager.emit('start', sourceDatabases) + + for (let i in sourceDatabases) { + const sourceDbInfo = sourceDatabases[i] + + // Don't migrate the special storage_database that is internally managed to maintain + // a list of all the databases in a context + if (sourceDbInfo.databaseName == 'storage_database') { + eventManager.emit('migrated', sourceDbInfo, parseInt(i) + 1, sourceDatabases.length) + continue + } + + try { + // Open source and destination databases + + const sourceConfig = { + permissions: sourceDbInfo.permissions, + verifyEncryptionKey: false + } + const sourceDb = await sourceContext.openDatabase(sourceDbInfo.databaseName, sourceConfig) + + const destinationConfig = { + permissions: sourceDbInfo.permissions, + verifyEncryptionKey: false + } + const destinationDb = await destinationContext.openDatabase(sourceDbInfo.databaseName, destinationConfig) + + // Migrate data + await migrateDatabase(sourceDb, destinationDb) + + // Close databases + await sourceDb.close() + await destinationDb.close() + + // Emit success event + eventManager.emit('migrated', sourceDbInfo, parseInt(i) + 1, sourceDatabases.length) + } catch (err: any) { + eventManager.emit('error', err.message) + return + } + } + + eventManager.emit('complete') +} + +export async function migrateDatabase(sourceDb: IDatabase, destinationDb: IDatabase): Promise { + // Loop through all records in the source database and save them to the destination database + // We do this to ensure the data is re-encrypted using the correct key of the destination database + // If we used pouchdb in-built replication, the data would be migrated to a database with an incorrect + // encryption key + + const limit = 1 + let skip = 0 + while (true) { + const records = await sourceDb.getMany({}, { + limit, + skip + }) + + for (let r in records) { + const record: any = records[r] + + // Delete revision info so the record saves correctly + delete record['_rev'] + try { + await destinationDb.save(records[r]) + } catch (err: any) { + if (err.status != 409) { + throw err + } + } + } + + if (records.length == 0 || records.length < limit) { + // All data migrated + break + } + + skip += limit + } +} diff --git a/packages/client-ts/test/client.contexthash.tests.ts b/packages/client-ts/test/client.contexthash.tests.ts new file mode 100644 index 00000000..04dd6762 --- /dev/null +++ b/packages/client-ts/test/client.contexthash.tests.ts @@ -0,0 +1,59 @@ +const assert = require('assert') + +import { Client } from '../src/index' +import { AutoAccount } from '@verida/account-node' +import { StorageLink } from '@verida/storage-link' +import { DIDDocument } from '@verida/did-document' +import CONFIG from './config' +import { EnvironmentType, IDatabase } from '@verida/types' + +const CONTEXT_NAME = 'Verida Storage Node Test: Test Application 1' + +const PRIVATE_KEY = '' +const ENVIRONMENT = EnvironmentType.TESTNET + +let client, account, did + +/** + * Test a single (or collection) of storage nodes + */ +describe('Storage context hash tests', function() { + + this.beforeAll(async function() { + client = new Client({ + environment: ENVIRONMENT, + didClientConfig: { + network: ENVIRONMENT, + } + }) + + account = new AutoAccount({ + privateKey: PRIVATE_KEY, + environment: ENVIRONMENT, + didClientConfig: CONFIG.DID_CLIENT_CONFIG + }) + + await client.connect(account) + did = await account.did() + }) + + describe.skip('Perform tests', () =>{ + + it('can fetch correct context name', async function () { + const contextHash = DIDDocument.generateContextHash(did, CONTEXT_NAME); + const contextName = await client.getContextNameFromHash(contextHash) + assert.equal(contextName, CONTEXT_NAME, 'Context name matches expected value') + }) + + it(`can't fetch incorrect context name`, async function () { + const contextHash = '0xinvalidvalue'; + + try { + await client.getContextNameFromHash(contextHash) + assert.fail('Should have failed') + } catch (err) { + assert.equal(err.message, `Unable to locate service associated with context hash ${contextHash}`) + } + }) + }) +}) \ No newline at end of file diff --git a/packages/client-ts/test/client.destroy.tests.ts b/packages/client-ts/test/client.destroy.tests.ts new file mode 100644 index 00000000..780e21cd --- /dev/null +++ b/packages/client-ts/test/client.destroy.tests.ts @@ -0,0 +1,118 @@ +const assert = require('assert') + +import { Client } from '../src/index' +import { AutoAccount } from '@verida/account-node' +import CONFIG from './config' +import { EnvironmentType, IDatabase } from '@verida/types' + +const CONTEXT_NAME = 'Verida Test: Destroyer' + +const PRIVATE_KEY = '' +const ENVIRONMENT = EnvironmentType.TESTNET + +let client, account, did + +describe('Destroy account and context tests', function() { + + this.beforeAll(async function() { + const localStorageNodeUri = 'http://localhost:5000/' + + client = new Client({ + environment: ENVIRONMENT, + didClientConfig: { + network: ENVIRONMENT, + } + }) + + account = new AutoAccount({ + privateKey: PRIVATE_KEY, + environment: ENVIRONMENT, + didClientConfig: CONFIG.DID_CLIENT_CONFIG + }, { + defaultDatabaseServer: { + type: 'VeridaDatabase', + endpointUri: [localStorageNodeUri] + }, + defaultMessageServer: { + type: 'VeridaMessage', + endpointUri: [localStorageNodeUri] + }, + }) + + await client.connect(account) + did = await account.did() + console.log(`DID: ${did}`) + }) + + describe.skip('Destroy stuff', () =>{ + + it('can open and destroy a context', async function () { + // open a context and save some data + console.log('opening context') + const context = await client.openContext(CONTEXT_NAME) + + console.log('opening database and saving data') + const db = await context.openDatabase('testDb') + await db.save({hello: 1}) + + const info = await db.info() + console.log(info) + + // close database + console.log('closing databaes') + await db.close({ + clearLocal: true + }) + + // close context + console.log('closing context') + await context.close({ + clearLocal: true + }) + + // destroy context + console.log(`destroying context ${CONTEXT_NAME} / ${did}`) + const endpointResults = await client.destroyContext(CONTEXT_NAME) + + for (let endpointUri in endpointResults) { + const result = endpointResults[endpointUri] + if (result.status == 'rejected') { + console.log(result.reason.response.data) + assert.fail(result.reason.toString()) + } + } + + // @todo: check storage node has deleted the databases and context database correctly (checked manually). could check the context hash doesn't exist via /user/contextHash + // @todo: check DID document doesn't have the context (checked manually) + }) + + it(`can destroy an account`, async function() { + // open a context and save some data + console.log('opening context') + const context = await client.openContext(CONTEXT_NAME) + + console.log('opening database and saving data') + const db = await context.openDatabase('testDb') + await db.save({hello: 1}) + + const info = await db.info() + console.log(info) + + // close database + console.log('closing database') + await db.close({ + clearLocal: true + }) + + // close context + console.log('closing context') + await context.close({ + clearLocal: true + }) + + await client.destroyAccount() + + // @todo: check DID is deleted (checked manually) + }) + }) +}) \ No newline at end of file diff --git a/packages/client-ts/test/context.hash.tests.ts b/packages/client-ts/test/context.hash.tests.ts new file mode 100644 index 00000000..a4d02048 --- /dev/null +++ b/packages/client-ts/test/context.hash.tests.ts @@ -0,0 +1,151 @@ + + +'use strict' +const assert = require('assert') + +import { Client } from '../src/index' +import { AutoAccount } from '@verida/account-node' +import { StorageLink } from '@verida/storage-link' +import { DIDDocument } from '@verida/did-document' +import CONFIG from './config' +import { EnvironmentType, IDatabase } from '@verida/types' + +const TEST_DB_NAME = 'TestDb_1' +const CONTEXT_NAME = 'Verida Test: Context Hash' + +const PRIVATE_KEY = '0x002efd2e44f0d2cbbb71506a02a2043ba45f222f04b501f139f29a0d3b21f003' +const ENVIRONMENT = EnvironmentType.DEVNET + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) + +/** + * Things to test + * + * 1. Open context by name or hash, returns the same databases + * 2. If context hash is supplied, unable to create a new context (should only create with the correct name) + */ + +const saveAndVerify = async (database: any) => { + console.log(`Attempting to save a record`) + const result = await database.save({'hello': 'world'}) + console.log(`Record saved`, result) + console.log(`Fetching records`) + const data = await database.getMany({ + _id: result.id + }) + assert.ok(data, 'Data returned') + console.log(`Records fetched`) + + assert.ok(data.length && data.length > 0, 'Array returned with at least one row') + assert.ok(data[0].hello == 'world', 'First result has expected value') +} + +/** + * Test a single (or collection) of storage nodes + */ +describe('Storage context hash tests', () => { + let didClient, contextByName, contextByHash + + const client = new Client({ + environment: CONFIG.ENVIRONMENT, + didClientConfig: { + network: ENVIRONMENT, + } + }) + + describe('Perform tests', function() { + this.timeout(200 * 1000) + + it(`can open same context with either name or hash`, async function() { + const account = new AutoAccount({ + privateKey: PRIVATE_KEY, + environment: ENVIRONMENT, + didClientConfig: { + ...CONFIG.DID_CLIENT_CONFIG, + } + }) + await client.connect(account) + await account.loadDefaultStorageNodes('AU') + didClient = await account.getDidClient() + const did = await account.did() + + console.log(`Opening context (by name) for DID: ${did}`) + contextByName = await client.openContext(CONTEXT_NAME, true) + assert.ok(context, 'Account context (by name) opened') + console.log(`Context (by name) opened`) + + // Open database + console.log(`Attempting to open a database`) + const database = await contextByName.openDatabase(TEST_DB_NAME) + + // Get database info + const dbInfo = await database.info() + console.log(`Database info`) + console.log(dbInfo) + + // Save and verify a record + console.log(`Saving a record to context (by name)`) + const result = await database.save({'context': 'by name'}) + const contextNameData = await database.getMany({ + _id: result.id + }) + assert.ok(contextNameData, 'Data returned') + assert.ok(contextNameData.length && contextNameData.length > 0, 'Array returned with at least one row') + assert.ok(contextNameData[0].context == 'by name', 'First result has expected value') + console.log('Save complete') + + // Close context by name + await contextByName.close() + console.log(`Closed context by name`) + + // Open hash context + const contextHash = DIDDocument.generateContextHash(did, CONTEXT_NAME); + + console.log(`Opening context (by name) for DID: ${did}`) + contextByName = await client.openContext(contextHash, false) + assert.ok(context, 'Account context (by hash) opened') + console.log(`Context (by hash) opened`) + + // Open database + console.log(`Attempting to open a database`) + const database2 = await contextByName.openDatabase(TEST_DB_NAME) + + // Confirm the context name record exists + const contextTestData = await database.getMany({ + _id: result.id + }) + assert.ok(contextTestData, 'Data returned') + assert.ok(contextTestData.length && contextTestData.length > 0, 'Array returned with at least one row') + assert.ok(contextTestData[0].context == 'by name', 'First result has expected value') + + + // Get database info + const dbInfo2 = await database2.info() + console.log(`Database info`) + console.log(dbInfo2) + + // Save and verify a record + console.log(`Saving a record to context (by name)`) + const result2 = await database2.save({'context': 'by hash'}) + const contextHashData = await database2.getMany({ + _id: result2.id + }) + assert.ok(contextHashData, 'Data returned') + assert.ok(contextHashData.length && contextHashData.length > 0, 'Array returned with at least one row') + assert.ok(contextHashData[0].context == 'by hash', 'First result has expected value') + console.log('Save complete') + + // Delete database + await contextByHash.deleteDatabase(TEST_DB_NAME) + console.log(`${TEST_DB_NAME} database deleted`) + + await contextByHash.close({ + clearLocal: true + }) + }) + }) + + after(async () => { + + }) +}) \ No newline at end of file diff --git a/packages/client-ts/test/migration.tests.ts b/packages/client-ts/test/migration.tests.ts new file mode 100644 index 00000000..62bee6d2 --- /dev/null +++ b/packages/client-ts/test/migration.tests.ts @@ -0,0 +1,259 @@ +'use strict' +const assert = require('assert') + +import { Client } from '../src/index' +import { AutoAccount } from '@verida/account-node' +import CONFIG from './config' +import { EnvironmentType } from '@verida/types' +import { migrateContext } from '../src/utils/migration' + +const TEST_DBS = ['db1-test', 'db2-test', 'db3-test'] + +const SOURCE_CONTEXT_NAME = 'Verida Test: Migration - Source Context' +const DESTINATION_CONTEXT_NAME = 'Verida Test: Migration - Destination Context' + +/** + * + */ +describe('Storage context tests', () => { + let sourceContext, destinationContext + + const client1 = new Client({ + environment: CONFIG.ENVIRONMENT, + didClientConfig: { + network: EnvironmentType.TESTNET, + rpcUrl: CONFIG.DID_CLIENT_CONFIG.rpcUrl + } + }) + + const client2 = new Client({ + environment: CONFIG.ENVIRONMENT, + didClientConfig: { + network: EnvironmentType.TESTNET, + rpcUrl: CONFIG.DID_CLIENT_CONFIG.rpcUrl + } + }) + + describe('Initialize user storage contexts', function() { + this.timeout(200 * 1000) + + it(`can open the source and destination application contexts`, async function() { + const account1 = new AutoAccount({ + privateKey: CONFIG.VDA_PRIVATE_KEY, + environment: CONFIG.ENVIRONMENT, + didClientConfig: CONFIG.DID_CLIENT_CONFIG + }) + const account2 = new AutoAccount({ + privateKey: CONFIG.VDA_PRIVATE_KEY, + environment: CONFIG.ENVIRONMENT, + didClientConfig: CONFIG.DID_CLIENT_CONFIG + }) + + await client1.connect(account1) + await client2.connect(account2) + + sourceContext = await client1.openContext(SOURCE_CONTEXT_NAME, true) + assert.ok(context, 'Source context opened') + + destinationContext = await client2.openContext(DESTINATION_CONTEXT_NAME, true) + assert.ok(context, 'Destination context opened') + }) + + it('can create test database data', async function() { + for (let i in TEST_DBS) { + const dbName = TEST_DBS[i] + const db = await sourceContext.openDatabase(dbName) + await db.save({record: 1}) + await db.save({record: 2}) + await db.save({record: 3}) + await db.close() + } + + assert.ok(true, 'Test database data created') + }) + + it('can migrate data from source context to the destination context', async function() { + const events = migrateContext(sourceContext, destinationContext) + + events.on('start', (databases: object) => { + console.log('Migration starting with databases:') + console.log(databases) + }) + + events.on('migrated', (dbInfo, dbIndex, totalDbs) => { + const percentComplete = (dbIndex) / totalDbs * 100 + console.log(`Migrated database ${dbInfo.databaseName} (${dbIndex}/${totalDbs}) (${percentComplete}%)`) + }) + + const promise = new Promise((resolve, rejects) => { + events.on('complete', () => { + console.log('Migration complete!') + resolve(true) + }) + + events.on('error', (err: any) => { + console.log('Migration error!') + console.log(err) + rejects(err) + }) + }) + + try { + const result = await promise + assert.ok(true, 'Data migrated') + } catch (err) { + assert.fail(err.message) + } + }) + + it('can verify database data matches exactly', async function() { + // Close and re-open contexts to reset everything + await sourceContext.close({ + clearLocal: true + }) + await destinationContext.close({ + clearLocal: true + }) + + sourceContext = await client1.openContext(SOURCE_CONTEXT_NAME) + destinationContext = await client2.openContext(DESTINATION_CONTEXT_NAME) + + // Verify data for all databases + for (let i in TEST_DBS) { + const dbName = TEST_DBS[i] + + try { + const sourceDb = await sourceContext.openDatabase(dbName, { + verifyEncryptionKey: false + }) + const destinationDb = await destinationContext.openDatabase(dbName, { + verifyEncryptionKey: false + }) + + const sourceRows = await sourceDb.getMany() + const destinationRows = await destinationDb.getMany() + + // Verify the same number of rows returned + assert.equal(destinationRows.length, sourceRows.length, `${dbName}: source and destination databases have same length`) + + // Verify the same row IDs are returned + const sourceIds = sourceRows.map((item) => item.id) + const destinationIds = destinationRows.map((item) => item.id) + assert.deepEqual(sourceIds, destinationIds, `${dbName}: source and destination databases have same records`) + } catch (err) { + console.log(dbName) + console.log(err) + assert.fail(err.message) + } + } + }) + + /** + * When re-starting an existing migration, any changes to the source data that has already + * been sent to the destination will not be updated. This also includes deletions on the + * source data that was already migrated that is then deleted, will not be deleted on the destination + */ + it('can partially migrate, then fully complete', async function() { + // Close and re-open contexts to reset everything + await sourceContext.close({ + clearLocal: true + }) + await destinationContext.close({ + clearLocal: true + }) + + // Just work with the first test database + const dbName = TEST_DBS[0] + + // Re-open application contexts + sourceContext = await client1.openContext(SOURCE_CONTEXT_NAME) + destinationContext = await client2.openContext(DESTINATION_CONTEXT_NAME) + + // Add a new record + const db = await sourceContext.openDatabase(dbName) + await db.save({record: 4}) + + // Re-run the migration + const events = migrateContext(sourceContext, destinationContext) + const promise = new Promise((resolve, rejects) => { + events.on('complete', () => { + console.log('Migration complete!') + resolve(true) + }) + + events.on('error', (err: any) => { + console.log('Migration error!') + console.log(err) + rejects(err) + }) + }) + + try { + await promise + assert.ok(true, 'Data migrated') + } catch (err) { + assert.fail(err.message) + } + + // Verify the data in the first database is correct + try { + const sourceDb = await sourceContext.openDatabase(dbName, { + verifyEncryptionKey: false + }) + const destinationDb = await destinationContext.openDatabase(dbName, { + verifyEncryptionKey: false + }) + + const sourceRows = await sourceDb.getMany() + const destinationRows = await destinationDb.getMany() + + // Verify the same number of rows returned + assert.equal(destinationRows.length, sourceRows.length, `${dbName}: source and destination databases have same length`) + + // Verify the same row IDs are returned + const sourceIds = sourceRows.map((item) => item.id) + const destinationIds = destinationRows.map((item) => item.id) + assert.deepEqual(sourceIds, destinationIds, `${dbName}: source and destination databases have same records`) + + await sourceDb.close({ + clearLocal: true + }) + await destinationDb.close({ + clearLocal: true + }) + } catch (err) { + assert.fail(err.message) + } + }) + }) + + after(async () => { + if (sourceContext) { + for (let i in TEST_DBS) { + const dbName = TEST_DBS[i] + + // Delete databases + try { + await sourceContext.deleteDatabase(dbName) + } catch (err) { + console.log(err.message) + } + + try { + await destinationContext.deleteDatabase(dbName) + } catch (err) { + console.log(err.message) + } + } + + // Close contexts + await sourceContext.close({ + clearLocal: true + }) + + await destinationContext.close({ + clearLocal: true + }) + } + }) +}) \ No newline at end of file diff --git a/packages/client-ts/test/storage.failure.tests.ts b/packages/client-ts/test/storage.failure.tests.ts new file mode 100644 index 00000000..58dc2fdf --- /dev/null +++ b/packages/client-ts/test/storage.failure.tests.ts @@ -0,0 +1,169 @@ +'use strict' +const assert = require('assert') + +import { Client } from '../src/index' +import { AutoAccount } from '@verida/account-node' +import CONFIG from './config' +import { EnvironmentType, IDatabase } from '@verida/types' + +const TEST_DB_NAME = 'TestDb_1' +const CONTEXT_NAME = 'Verida Test: Node failure' +//const CONTEXT_NAME = '0xaf76137db7f06af84bca9ecf9666846c61c41b2fcad80e435f1aa3897a8426a0' + +const PRIVATE_KEY = '0x002efd2e44f0d2cbbb71506a02a2043ba45f222f04b501f139f29a0d3b21f002' +const ENVIRONMENT = EnvironmentType.DEVNET + +const SLEEP_SECONDS = 30 +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) + +/** + * Things to test: + * + * 1. (Y) Successfully open a database connection when a node is unavailable. + * 2. (Fixed) Successfully save database data to a different server if the currently connected server is unavailable + * 3. (Fixed) Successfully delete a database with a node down, the database is removed from all nodes that are live, even when one node is down + * 3. (No, unsure how to achieve this) Successfully delete a database with a node down, the database is removed when the node comes back online + * 4. (Y) When connecting, a random node is selected, not the first node (Yes, stays the same for a 1 hr period) + * 5. (Fixed) Replication on the server when a node is down, is working correctly + */ + +const saveAndVerify = async (database: any) => { + console.log(`Attempting to save a record`) + const result = await database.save({'hello': 'world'}) + console.log(`Record saved`, result) + console.log(`Fetching records`) + const data = await database.getMany({ + _id: result.id + }) + assert.ok(data, 'Data returned') + console.log(`Records fetched`) + + assert.ok(data.length && data.length > 0, 'Array returned with at least one row') + assert.ok(data[0].hello == 'world', 'First result has expected value') +} + +/** + * Test a single (or collection) of storage nodes + */ +describe('Storage node failure tests', () => { + let didClient, context + + const client = new Client({ + environment: CONFIG.ENVIRONMENT, + didClientConfig: { + network: ENVIRONMENT, + } + }) + + describe.skip('Failures', function() { + this.timeout(200 * 1000) + + it(`can use a database with a node down`, async function() { + // Note: Stop one of the storage nodes for the DID before running this test + console.log(`Ensure you have stopped one of the storage nodes associated with this DID / Context before running this test.`) + + const account = new AutoAccount({ + privateKey: PRIVATE_KEY, + environment: ENVIRONMENT, + didClientConfig: { + ...CONFIG.DID_CLIENT_CONFIG, + } + }) + await client.connect(account) + await account.loadDefaultStorageNodes('AU') + didClient = await account.getDidClient() + const did = await account.did() + + console.log(`Opening context for DID: ${did}`) + context = await client.openContext(CONTEXT_NAME, true) + assert.ok(context, 'Account context opened') + console.log(`Context opened`) + + // Open database + console.log(`Attempting to open a database`) + const database = await context.openDatabase(TEST_DB_NAME) + + // Get database info + const dbInfo = await database.info() + console.log(`Database info`) + console.log(dbInfo) + + // Save and verify a record + await saveAndVerify(database) + console.log('Complete') + + // Delete database + await context.deleteDatabase(TEST_DB_NAME) + console.log(`${TEST_DB_NAME} database deleted`) + }) + + it(`can failover if a connected node goes down`, async function() { + const account = new AutoAccount({ + privateKey: PRIVATE_KEY, + environment: ENVIRONMENT, + didClientConfig: { + ...CONFIG.DID_CLIENT_CONFIG, + } + }) + await client.connect(account) + await account.loadDefaultStorageNodes('AU') + didClient = await account.getDidClient() + const did = await account.did() + + console.log(`Opening context for DID: ${did}`) + context = await client.openContext(CONTEXT_NAME, true) + assert.ok(context, 'Account context opened') + console.log(`Context opened`) + + // Open database + console.log(`Attempting to open a database`) + const database = await context.openDatabase(TEST_DB_NAME) + + const sourceDbEngine = await context.getDatabaseEngine(did) + const sourceDbInfo = await sourceDbEngine.info() + console.log(sourceDbInfo) + + const data = await database.getMany() + console.log(data) + + // Get database info + const dbInfo = await database.info() + console.log(`Database info`) + console.log(dbInfo) + + // Save and verify a record + await saveAndVerify(database) + + // Sleep for 60 seconds so server can be manually shut down + console.log(`Sleeping for ${SLEEP_SECONDS} seconds, shut down the node (${dbInfo.endpoint})`) + await sleep(SLEEP_SECONDS * 1000) + + // Save and verify another record + await saveAndVerify(database) + + // Get database info + const dbInfo2 = await database.info() + console.log(`Database info`) + console.log(dbInfo2) + + const finalData = await database.getMany() + console.log('all data:') + console.log(finalData) + + console.log('Closing the database') + await database.close({ + clearLocal: true + }) + + // Delete database + await context.deleteDatabase(TEST_DB_NAME) + console.log(`${TEST_DB_NAME} database deleted`) + }) + }) + + after(async () => { + await context.close({ + clearLocal: true + }) + }) +}) \ No newline at end of file diff --git a/packages/did-client/CHANGELOG.md b/packages/did-client/CHANGELOG.md index 59ff66a1..be9d0311 100644 --- a/packages/did-client/CHANGELOG.md +++ b/packages/did-client/CHANGELOG.md @@ -1,3 +1,9 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Feature: Make `web3config` auto-populate `RPC_URL` and other defaults based on Verida environment +- Feature: Support easy method to destroy a DID + 2023-04-20 (v2.3.0) ------------------- diff --git a/packages/did-client/package.json b/packages/did-client/package.json index 3d222f67..469777ac 100644 --- a/packages/did-client/package.json +++ b/packages/did-client/package.json @@ -1,6 +1,6 @@ { "name": "@verida/did-client", - "version": "2.3.9", + "version": "2.4.0-rc6", "main": "dist/index.js", "license": "ISC", "directories": { @@ -17,11 +17,11 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/did-document": "^2.3.3", - "@verida/types": "^2.3.1", - "@verida/vda-common": "^2.3.6", - "@verida/vda-did-resolver": "^2.3.9", - "@verida/web3": "^2.3.6", + "@verida/did-document": "^2.4.0-rc1", + "@verida/types": "^2.4.0-rc1", + "@verida/vda-common": "^2.4.0-rc5", + "@verida/vda-did-resolver": "^2.4.0-rc5", + "@verida/web3": "^2.4.0-rc5", "axios": "^0.23.0", "deepcopy": "^2.1.0", "did-resolver": "^4.0.1", diff --git a/packages/did-client/src/did-client.ts b/packages/did-client/src/did-client.ts index c7b5d1ae..87a040fd 100644 --- a/packages/did-client/src/did-client.ts +++ b/packages/did-client/src/did-client.ts @@ -2,7 +2,7 @@ import { DIDDocument as VeridaDIDDocument } from "@verida/did-document" import { default as VeridaWallet } from "./wallet" import { getResolver } from '@verida/vda-did-resolver' -import { RPC_URLS } from "@verida/vda-common" +import { RPC_URLS, getWeb3ConfigDefaults } from "@verida/vda-common" import { VdaDid } from '@verida/vda-did' import { Resolver } from 'did-resolver' import { Web3CallType, DIDClientConfig, VdaDidEndpointResponses, Web3ResolverConfigurationOptions, Web3SelfTransactionConfig, Web3MetaTransactionConfig, VeridaWeb3ConfigurationOptions, Web3SelfTransactionConfigPart, IDIDClient, VeridaDocInterface } from "@verida/types" @@ -30,9 +30,7 @@ export class DIDClient implements IDIDClient { timeout: config.timeout ? config.timeout : 10000 } - if (this.config.rpcUrl) { - resolverConfig.rpcUrl = this.config.rpcUrl - } + resolverConfig.rpcUrl = this.getRpcUrl() const vdaDidResolver = getResolver(resolverConfig) // @ts-ignore @@ -75,6 +73,11 @@ export class DIDClient implements IDIDClient { throw new Error('Web3 transactions must specify `web3config.privateKey`') } + web3Config = { + ...getWeb3ConfigDefaults(this.config.network), + ...web3Config + } + // @ts-ignore let rpcUrl = web3Config.rpcUrl || this.config.rpcUrl if (callType == 'web3' && !rpcUrl) { @@ -122,6 +125,19 @@ export class DIDClient implements IDIDClient { return undefined } + /** + * Destroy this DID + * + * Note: This can not be reversed and is written to the blockchain + */ + public async destroy(): Promise { + if (!this.authenticated()) { + throw new Error("Unable to save DIDDocument. No private key.") + } + + return await this.vdaDid!.delete() + } + /** * Save DIDDocument to the chain * diff --git a/packages/did-document/package.json b/packages/did-document/package.json index d02d7b4d..6ae10fc4 100644 --- a/packages/did-document/package.json +++ b/packages/did-document/package.json @@ -1,6 +1,6 @@ { "name": "@verida/did-document", - "version": "2.3.3", + "version": "2.4.0-rc1", "main": "dist/index.js", "license": "ISC", "directories": { @@ -20,9 +20,9 @@ "@ethersproject/address": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", "@ethersproject/transactions": "^5.7.0", - "@verida/encryption-utils": "^2.2.3", - "@verida/keyring": "^2.3.3", - "@verida/types": "^2.3.1", + "@verida/encryption-utils": "^2.2.2", + "@verida/keyring": "^2.4.0-rc1", + "@verida/types": "^2.4.0-rc1", "did-resolver": "^4.0.1", "lodash": "^4.17.21" }, diff --git a/packages/helpers/CHANGELOG.md b/packages/helpers/CHANGELOG.md index 832aea37..08d336f6 100644 --- a/packages/helpers/CHANGELOG.md +++ b/packages/helpers/CHANGELOG.md @@ -1,3 +1,8 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Support `verifyDidControlsDomain()` helper + 2023-04-20 (v2.3.0) ------------------- diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 71d94cd9..a736432f 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@verida/helpers", - "version": "2.3.3", + "version": "2.4.0-rc2", "main": "dist/index.js", "license": "ISC", "directories": { @@ -17,8 +17,9 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/encryption-utils": "^2.2.3", - "@verida/types": "^2.3.1", + "@verida/encryption-utils": "^2.2.2", + "@verida/types": "^2.4.0-rc1", + "axios": "^1.6.2", "bs58": "^5.0.0", "url": "^0.11.0" }, diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index 79d7ba4f..ed7f19b6 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -1 +1,2 @@ -export * from "./Utils" \ No newline at end of file +export * from "./Utils" +export * from "./verification" \ No newline at end of file diff --git a/packages/helpers/src/verification.ts b/packages/helpers/src/verification.ts new file mode 100644 index 00000000..0b6ebec1 --- /dev/null +++ b/packages/helpers/src/verification.ts @@ -0,0 +1,40 @@ +import Axios from 'axios' + +/** + * Uses `/.well-known/did.json` standard + * + * @see https://w3c-ccg.github.io/did-method-web/ + * @see https://team.verida.network/.well-known/did.json + * + * @param did DID that is expected to control the domain name + * @param domain Domain (ie: team.verida.network) that is expected to be controlled by the DID. If protocol is specified (ie: `https`) it will automatically be stripped. HTTPS is forced. + */ +export async function verifyDidControlsDomain(did: string, domain: string): Promise { + // Strip out protocol if specified + domain = domain.replace(/^https?:\/\//, '') + // Remove any trailing '/' + domain = domain.replace(/\/$/,'') + // Force SSL + const didJsonUrl = `https://${domain}/.well-known/did.json` + try { + const response = await Axios.get(didJsonUrl) + const didJson = response.data + if (didJson.id !== `did:web:${domain}`) { + return false + } + + const match = didJson.verificationMethod!.find((entry: any) => { + return ( + // Verify authentication and entry ID match the domain + entry.id.match(`did:web:${domain}`) && + didJson.authentication.find((authEntry: any) => authEntry == entry.id) && + // Verify the entry matches the DID + entry.controller.toLowerCase().match(`${did.toLowerCase()}`) + ) + }) + + return match != undefined + } catch (err) { + return false + } +} \ No newline at end of file diff --git a/packages/helpers/test/verification.test.ts b/packages/helpers/test/verification.test.ts new file mode 100644 index 00000000..e44adcfb --- /dev/null +++ b/packages/helpers/test/verification.test.ts @@ -0,0 +1,32 @@ +const assert = require('assert') +import { verifyDidControlsDomain } from '../src/index' + +const DID = 'did:vda:testnet:0x0Ece1EefE84d77951d6697558cba50774854b9E6' +const DOMAIN = 'team.verida.network' +const URI = 'http://team.verida.network' + +describe('Verification tests', () => { + + describe(`Verify domain ${DOMAIN}`, function() { + it('can verify domain without slash', async () => { + const result = await verifyDidControlsDomain(DID, DOMAIN) + assert(result, 'Domain without slash verifies correctly ') + }) + + it('can verify domain with slash', async () => { + const result = await verifyDidControlsDomain(DID, `${DOMAIN}/`) + assert(result, 'Domain with slash verifies correctly ') + }) + + it('can verify domain from URI', async () => { + const result = await verifyDidControlsDomain(DID, URI) + assert(result, 'URI verifies correctly ') + }) + + it('can verify with lower case DID', async () => { + const result = await verifyDidControlsDomain(DID.toLowerCase(), DOMAIN) + assert(result, 'Lower case DID verifies correctly ') + }) + + }) +}) \ No newline at end of file diff --git a/packages/keyring/package.json b/packages/keyring/package.json index 67475e25..5983a21a 100644 --- a/packages/keyring/package.json +++ b/packages/keyring/package.json @@ -1,6 +1,6 @@ { "name": "@verida/keyring", - "version": "2.3.3", + "version": "2.4.0-rc1", "main": "dist/index.js", "license": "ISC", "directories": { @@ -16,8 +16,8 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/encryption-utils": "^2.2.3", - "@verida/types": "^2.3.1", + "@verida/encryption-utils": "^2.2.2", + "@verida/types": "^2.4.0-rc1", "ethers": "^5.5.1", "tweetnacl": "^1.0.3", "uuid": "^8.3.2" diff --git a/packages/storage-link/package.json b/packages/storage-link/package.json index a8a204fb..7d3b69ac 100644 --- a/packages/storage-link/package.json +++ b/packages/storage-link/package.json @@ -1,6 +1,6 @@ { "name": "@verida/storage-link", - "version": "2.3.9", + "version": "2.4.0-rc6", "main": "dist/index.js", "license": "ISC", "directories": { @@ -17,10 +17,10 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/did-client": "^2.3.9", - "@verida/did-document": "^2.3.3", - "@verida/encryption-utils": "^2.2.3", - "@verida/keyring": "^2.3.3", + "@verida/did-client": "^2.4.0-rc6", + "@verida/did-document": "^2.4.0-rc1", + "@verida/encryption-utils": "^2.2.2", + "@verida/keyring": "^2.4.0-rc1", "did-resolver": "^4.0.0", "url-parse": "^1.5.3" }, diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 9b39564a..11a6da54 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,3 +1,10 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Added types for `StorageNode` +- Feature: Added EIP1559 gas configuration to `Web3Interface` +- Make optional database params, actually optional with typescript + 2023-04-20 (v2.3.0) ------------------- diff --git a/packages/types/src/ContextInterfaces.ts b/packages/types/src/ContextInterfaces.ts index 5685322c..87d7ee7b 100644 --- a/packages/types/src/ContextInterfaces.ts +++ b/packages/types/src/ContextInterfaces.ts @@ -69,6 +69,11 @@ export interface DatabaseOpenConfig { * Ignore any cached instance already created */ ignoreCache?: boolean + + /** + * Verify the encryption key is correct when opening the database + */ + verifyEncryptionKey?: boolean } export interface DatabaseDeleteConfig { diff --git a/packages/types/src/IDatabase.ts b/packages/types/src/IDatabase.ts index cdec1704..e6a7df88 100644 --- a/packages/types/src/IDatabase.ts +++ b/packages/types/src/IDatabase.ts @@ -24,9 +24,9 @@ export interface IDatabase { registryEntry(): Promise - close(options: DatabaseCloseOptions): Promise + close(options?: DatabaseCloseOptions): Promise - destroy(options: DatabaseDeleteConfig): Promise + destroy(options?: DatabaseDeleteConfig): Promise usage(): Promise } \ No newline at end of file diff --git a/packages/types/src/IDatastore.ts b/packages/types/src/IDatastore.ts index e20109f8..e8ffec98 100644 --- a/packages/types/src/IDatastore.ts +++ b/packages/types/src/IDatastore.ts @@ -8,13 +8,13 @@ export interface IDatastore { save(data: any, options: any): Promise getMany( - customFilter: any, - options: any + customFilter?: any, + options?: any ): Promise getOne( - customFilter: any, - options: any + customFilter?: any, + options?: any ): Promise get(key: string, options: any): Promise @@ -34,5 +34,5 @@ export interface IDatastore { writeList: string[] ): Promise - close(options: DatabaseCloseOptions): Promise + close(options?: DatabaseCloseOptions): Promise } \ No newline at end of file diff --git a/packages/types/src/IStorageEngine.ts b/packages/types/src/IStorageEngine.ts index 3b9d3260..b3cdbc72 100644 --- a/packages/types/src/IStorageEngine.ts +++ b/packages/types/src/IStorageEngine.ts @@ -33,7 +33,7 @@ export interface IStorageEngine { deleteDatabase( databaseName: string, - config: DatabaseDeleteConfig + config?: DatabaseDeleteConfig ): Promise logout(): void diff --git a/packages/types/src/IStorageNode.ts b/packages/types/src/IStorageNode.ts new file mode 100644 index 00000000..c651bf7e --- /dev/null +++ b/packages/types/src/IStorageNode.ts @@ -0,0 +1,10 @@ + +/** + * Status of StoargeNode and DataCenter + * Used in `vda-node-manager` and `vda-node-client` packages + */ +export enum EnumStatus { + removed = 0, + removing, + active +} \ No newline at end of file diff --git a/packages/types/src/VdaClientConfig.ts b/packages/types/src/VdaClientConfig.ts new file mode 100644 index 00000000..e3af371f --- /dev/null +++ b/packages/types/src/VdaClientConfig.ts @@ -0,0 +1,17 @@ +import { EnvironmentType } from "./NetworkInterfaces"; +import { Web3CallType, Web3SelfTransactionConfig, Web3MetaTransactionConfig } from "./Web3Interfaces"; +/** + * Interface for vda-xxx-client instance creation. + * @param did: DID + * @param signKey: private key of DID. Used to generate signature in transactions to chains + * @param chainNameOrId: Target chain name or chain id. + * @param callType : VDA-DID run mode. Values from vda-did-resolver + * @param web3Options: Web3 configuration depending on call type. Values from vda-did-resolver + */ +export interface VdaClientConfig { + network: EnvironmentType + did?: string + signKey?: string + callType?: Web3CallType + web3Options?: Web3SelfTransactionConfig | Web3MetaTransactionConfig +} \ No newline at end of file diff --git a/packages/types/src/Web3Interfaces.ts b/packages/types/src/Web3Interfaces.ts index cbcb8bf5..72393cef 100644 --- a/packages/types/src/Web3Interfaces.ts +++ b/packages/types/src/Web3Interfaces.ts @@ -13,11 +13,25 @@ export interface Web3ContractInfo { logPerformance?: boolean } -/** Gas configuration */ +/** EIP1559 Gas Configuration speed */ +export type EIP1559GasMode = 'safeLow' | 'standard' | 'fast'; +/** Gas configuration + * + * eip1559Mode - optional - Once this parameter is set, all other parameters are not used. Gas information is pulled from network. + * + * maxFeePerGas - optional - Used for EIP1559 chains + * maxPriorityFeePerGas - optional - Used for EIP1559 chains + * gasLimit - optional - Used for non EIP1559 chains + * gasPrice - optional - Used for non EIP1559 chains +*/ export interface Web3GasConfiguration { - maxFeePerGas?: BigNumber - maxPriorityFeePerGas?: BigNumber - gasLimit?: BigNumber + eip1559Mode?: EIP1559GasMode; + eip1559gasStationUrl?: string; + + maxFeePerGas?: BigNumber; + maxPriorityFeePerGas?: BigNumber; + gasLimit?: BigNumber; + gasPrice?: BigNumber; } /** diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b547e8b9..dde580bb 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -19,4 +19,6 @@ export * from './IStorageEngine' export * from './StorageLinkInterfaces' export * from './Web3Interfaces' export * from './IAuthType' -export * from './IDbRegistry' \ No newline at end of file +export * from './IDbRegistry' +export * from './VdaClientConfig' +export * from './IStorageNode' \ No newline at end of file diff --git a/packages/vda-common-test/src/const.ts b/packages/vda-common-test/src/const.ts index d76d08ef..37f898c8 100644 --- a/packages/vda-common-test/src/const.ts +++ b/packages/vda-common-test/src/const.ts @@ -46,5 +46,4 @@ export const RECIPIENT_WALLET = { publicKey : '0x040f8ef908ca54fb1a45d8dd4463e6930c1d96c674d75f3c27e42d5f60ae2123837b47d1b249e8a015e3dcf2b669a7f30884a80ff57bf3332bf96698626f31a5da', }; -export const ZERO_ADDRESS = ethers.ZeroAddress - \ No newline at end of file +export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; \ No newline at end of file diff --git a/packages/vda-common/CHANGELOG.md b/packages/vda-common/CHANGELOG.md index 3d675f4d..f4ca2b76 100644 --- a/packages/vda-common/CHANGELOG.md +++ b/packages/vda-common/CHANGELOG.md @@ -1,3 +1,8 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Feature: Add gas station config for all networks + 2023-04-27 (v2.3.4) ------------------- diff --git a/packages/vda-common/package.json b/packages/vda-common/package.json index 4c14c56d..e9745e19 100644 --- a/packages/vda-common/package.json +++ b/packages/vda-common/package.json @@ -1,6 +1,6 @@ { "name": "@verida/vda-common", - "version": "2.3.6", + "version": "2.4.0-rc5", "description": "Common utils & contract addresses for Verida", "main": "dist/index.js", "author": "Alex J", diff --git a/packages/vda-common/src/abi/SoulboundNFT.json b/packages/vda-common/src/abi/SoulboundNFT.json index 620e3069..8436b59a 100644 --- a/packages/vda-common/src/abi/SoulboundNFT.json +++ b/packages/vda-common/src/abi/SoulboundNFT.json @@ -521,6 +521,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "didAddress", + "type": "address" + } + ], + "name": "isTrustedSigner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/vda-common/src/abi/VDARewardContract.json b/packages/vda-common/src/abi/VDARewardContract.json index a01aec3f..8c1dbe48 100644 --- a/packages/vda-common/src/abi/VDARewardContract.json +++ b/packages/vda-common/src/abi/VDARewardContract.json @@ -294,6 +294,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "didAddress", + "type": "address" + } + ], + "name": "isTrustedSigner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/vda-common/src/abi/VeridaDIDLinkage.json b/packages/vda-common/src/abi/VeridaDIDLinkage.json index 65d7954b..9981f834 100644 --- a/packages/vda-common/src/abi/VeridaDIDLinkage.json +++ b/packages/vda-common/src/abi/VeridaDIDLinkage.json @@ -244,6 +244,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "didAddress", + "type": "address" + } + ], + "name": "isTrustedSigner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/vda-common/src/abi/VeridaToken.json b/packages/vda-common/src/abi/VeridaToken.json index 7869d800..e535283e 100644 --- a/packages/vda-common/src/abi/VeridaToken.json +++ b/packages/vda-common/src/abi/VeridaToken.json @@ -3,6 +3,11 @@ "contractName": "VeridaToken", "sourceName": "contracts/VDA-V1.sol", "abi": [ + { + "inputs": [], + "name": "BurnNotAllowed", + "type": "error" + }, { "inputs": [], "name": "DuplicatedRequest", @@ -563,19 +568,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "burn", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "decimals", @@ -1003,6 +995,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [], "name": "paused", @@ -1201,6 +1200,13 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/vda-common/src/contract.ts b/packages/vda-common/src/contract.ts index 6912ccc3..3e4b058a 100644 --- a/packages/vda-common/src/contract.ts +++ b/packages/vda-common/src/contract.ts @@ -16,48 +16,48 @@ export interface CONTRACT_INFO { export const CONTRACT_ADDRESS : Record> = { "VeridaDIDRegistry": { - mainnet: null, - "0x89": null, + mainnet: "0x6FF180EF62FA57e611E91bdDaDadB6635D6b9Af7", + "0x89": "0x6FF180EF62FA57e611E91bdDaDadB6635D6b9Af7", testnet: "0x08CB4462958e6462Cc899862393f0b3bB6664efD", "0x13881": "0x08CB4462958e6462Cc899862393f0b3bB6664efD", devnet: "0x08CB4462958e6462Cc899862393f0b3bB6664efD", }, "NameRegistry": { - mainnet: null, - "0x89": null, + mainnet: "0xc9ce048b464034C53207Bf120bF85f317fdb38C8", + "0x89": "0xc9ce048b464034C53207Bf120bF85f317fdb38C8", testnet: "0x1dD6AAc1858100091BEbb867C7628DA639F7C16E", "0x13881": "0x1dD6AAc1858100091BEbb867C7628DA639F7C16E", devnet: "0x1dD6AAc1858100091BEbb867C7628DA639F7C16E", }, "SoulboundNFT" : { - mainnet: null, - "0x89": null, + mainnet: "0xB500418b5F47758903Ae02bfB3605cBd19062889", + "0x89": "0xB500418b5F47758903Ae02bfB3605cBd19062889", testnet: "0x7bf539E81e8beE06e3408359aC0867eD9C3bbD52", "0x13881": "0x7bf539E81e8beE06e3408359aC0867eD9C3bbD52", devnet: "0x7bf539E81e8beE06e3408359aC0867eD9C3bbD52", }, "VeridaDIDLinkage" : { - mainnet: null, - "0x89": null, + mainnet: "0x5916F97e31B77884d81bdA875b7686A988E0d517", + "0x89": "0x5916F97e31B77884d81bdA875b7686A988E0d517", testnet: "0xF394a23dc6777cAB3067566F27Ec5bdDD2D0bD2A", "0x13881": "0xF394a23dc6777cAB3067566F27Ec5bdDD2D0bD2A", devnet: "0xF394a23dc6777cAB3067566F27Ec5bdDD2D0bD2A", }, "VeridaToken" : { - mainnet: null, - "0x89": null, + mainnet: "0x683565196C3EAb450003C964D4bad1fd3068D4cC", + "0x89": "0x683565196C3EAb450003C964D4bad1fd3068D4cC", testnet: "0x745Db51351015d61573db37bC16C49B8506B93c8", "0x13881": "0x745Db51351015d61573db37bC16C49B8506B93c8", devnet: "0x745Db51351015d61573db37bC16C49B8506B93c8", }, "VDARewardContract": { - mainnet: null, - "0x89": null, + mainnet: "0xBAeEA910f6BBe29Ef33e0051e51dc60f9702B7b9", + "0x89": "0xBAeEA910f6BBe29Ef33e0051e51dc60f9702B7b9", testnet: "0x5044bba95ad5a526c83086966B00F5ebB47A6673", "0x13881": "0x5044bba95ad5a526c83086966B00F5ebB47A6673", devnet: "0x5044bba95ad5a526c83086966B00F5ebB47A6673", - } + }, }; export const CONTRACT_ABI : Record = { @@ -67,7 +67,7 @@ export const CONTRACT_ABI : Record = { "VeridaDIDLinkage": require('./abi/VeridaDIDLinkage.json'), "VeridaToken": require('./abi/VeridaToken.json'), - "VDARewardContract": require('./abi/VDARewardContract.json') + "VDARewardContract": require('./abi/VDARewardContract.json'), } export function getContractInfoForNetwork(name: CONTRACT_NAMES, chainNameOrId: string) : CONTRACT_INFO { diff --git a/packages/vda-common/src/defaults.ts b/packages/vda-common/src/defaults.ts new file mode 100644 index 00000000..92296c50 --- /dev/null +++ b/packages/vda-common/src/defaults.ts @@ -0,0 +1,24 @@ +import { RPC_URLS } from "./rpc" + +export function getWeb3ConfigDefaults(chainName: string) { + switch (chainName) { + case 'devnet': + return { + rpcUrl: RPC_URLS[chainName], + eip1559Mode: 'fast', + eip1559gasStationUrl: 'https://gasstation-testnet.polygon.technology/v2' + } + case 'testnet': + return { + rpcUrl: RPC_URLS[chainName], + eip1559Mode: 'fast', + eip1559gasStationUrl: 'https://gasstation-testnet.polygon.technology/v2' + } + case 'mainnet': + return { + rpcUrl: RPC_URLS[chainName], + eip1559Mode: 'fast', + eip1559gasStationUrl: 'https://gasstation.polygon.technology/v2' + } + } +} \ No newline at end of file diff --git a/packages/vda-common/src/index.ts b/packages/vda-common/src/index.ts index a7acdeb7..b2849550 100644 --- a/packages/vda-common/src/index.ts +++ b/packages/vda-common/src/index.ts @@ -1,3 +1,4 @@ export * from './contract' export * from './rpc' -export * from './utils' \ No newline at end of file +export * from './utils' +export * from './defaults' \ No newline at end of file diff --git a/packages/vda-common/src/rpc.ts b/packages/vda-common/src/rpc.ts index 22d984ba..2716319b 100644 --- a/packages/vda-common/src/rpc.ts +++ b/packages/vda-common/src/rpc.ts @@ -1,7 +1,7 @@ export const RPC_URLS: Record = { devnet: "https://rpc-mumbai.maticvigil.com", - mainnet: null, - "0x89": null, + mainnet: "https://polygon-rpc.com/", + "0x89": "https://polygon-rpc.com/", testnet: "https://rpc-mumbai.maticvigil.com", "0x13881": "https://rpc-mumbai.maticvigil.com", }; diff --git a/packages/vda-common/src/utils.ts b/packages/vda-common/src/utils.ts index 4b57c1b9..a1867ac0 100644 --- a/packages/vda-common/src/utils.ts +++ b/packages/vda-common/src/utils.ts @@ -1,4 +1,4 @@ -import { utils } from 'ethers' +import { BigNumberish, utils } from 'ethers' import EncryptionUtils from '@verida/encryption-utils' const { computeAddress, getAddress, solidityPack } = utils @@ -29,7 +29,7 @@ export function interpretIdentifier(identifier: string): { export function getVeridaSignWithNonce( rawMsg: string, privateKey: string, - nonce: number + nonce: BigNumberish ) { rawMsg = solidityPack(['bytes', 'uint256'], [rawMsg, nonce]); return getVeridaSign(rawMsg, privateKey); diff --git a/packages/vda-did-resolver/package.json b/packages/vda-did-resolver/package.json index 7bef4a52..7b93decd 100644 --- a/packages/vda-did-resolver/package.json +++ b/packages/vda-did-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@verida/vda-did-resolver", - "version": "2.3.9", + "version": "2.4.0-rc5", "main": "dist/index.js", "license": "ISC", "directories": { @@ -18,11 +18,11 @@ }, "dependencies": { "@ethersproject/providers": "^5.7.2", - "@verida/did-document": "^2.3.3", - "@verida/encryption-utils": "^2.2.3", - "@verida/types": "^2.3.1", - "@verida/vda-common": "^2.3.6", - "@verida/vda-did": "^2.3.6", + "@verida/did-document": "^2.4.0-rc1", + "@verida/encryption-utils": "^2.2.2", + "@verida/types": "^2.4.0-rc1", + "@verida/vda-common": "^2.4.0-rc5", + "@verida/vda-did": "^2.4.0-rc5", "axios": "1.2.0-alpha.1", "lodash": "^4.17.21" }, diff --git a/packages/vda-did/.env.example b/packages/vda-did/.env.example index f9c84c9d..beb93608 100644 --- a/packages/vda-did/.env.example +++ b/packages/vda-did/.env.example @@ -1,4 +1,7 @@ ## These are used for test only. PRIVATE_KEY = "43a..." -RPC_URL="https://rpc-mumbai.maticvigil.com" \ No newline at end of file +RPC_URL="https://rpc-mumbai.maticvigil.com" + +## Flag for Mainnet or Testnet. Used to get gas configuration in vda-web3-client +IS_PROD = true \ No newline at end of file diff --git a/packages/vda-did/CHANGELOG.md b/packages/vda-did/CHANGELOG.md index e4c5dadb..a10abffe 100644 --- a/packages/vda-did/CHANGELOG.md +++ b/packages/vda-did/CHANGELOG.md @@ -1,3 +1,13 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Fix: Ethers dependency was dev only +- Feature: Include on-chain error message in thrown exception +- Feature: Added manual gas configuration for the non-view contract functions +- Feature: Update test files for testnet with various gas configurations +- Feature: Added test file for mainnet test +- Fix: Maximum three retries when creating a DID to prevent infinite loops + 2023-04-27 (v2.3.4) ------------------- diff --git a/packages/vda-did/package.json b/packages/vda-did/package.json index 57a4812f..78710382 100644 --- a/packages/vda-did/package.json +++ b/packages/vda-did/package.json @@ -1,6 +1,6 @@ { "name": "@verida/vda-did", - "version": "2.3.6", + "version": "2.4.0-rc5", "main": "dist/index.js", "license": "ISC", "directories": { @@ -17,17 +17,17 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/did-document": "^2.3.3", - "@verida/encryption-utils": "^2.2.3", - "@verida/types": "^2.3.1", - "@verida/web3": "^2.3.6", + "@verida/did-document": "^2.4.0-rc1", + "@verida/encryption-utils": "^2.2.2", + "@verida/types": "^2.4.0-rc1", + "@verida/web3": "^2.4.0-rc5", "axios": "1.2.0-alpha.1", + "ethers": "^5.7.2", "lodash": "^4.17.21" }, "devDependencies": { "@verida/vda-common-test": "^2.3.0", "dotenv": "^16.0.3", - "ethers": "^5.7.2", "mocha": "7.0.0", "ts-mocha": "^8.0.0", "ts-node": "^9.1.1", diff --git a/packages/vda-did/src/blockchain/blockchainApi.ts b/packages/vda-did/src/blockchain/blockchainApi.ts index 9e138d5b..e8892ba5 100644 --- a/packages/vda-did/src/blockchain/blockchainApi.ts +++ b/packages/vda-did/src/blockchain/blockchainApi.ts @@ -1,6 +1,6 @@ import { interpretIdentifier, getContractInfoForNetwork } from "@verida/vda-common" import { getVeridaSignWithNonce } from "./helpers" -import { VdaDidConfigurationOptions } from "@verida/types" +import { VdaDidConfigurationOptions, Web3GasConfiguration } from "@verida/types" import { getVeridaContract, VeridaContract } from "@verida/web3" import { ethers } from "ethers" import EncryptionUtils from "@verida/encryption-utils" @@ -108,16 +108,21 @@ export default class BlockchainApi { * Register endpoints to blockchain * @param endpoints Array of endpoints to be registered */ - public async register(endpoints: string[]) { + public async register(endpoints: string[], gasConfig?: Web3GasConfiguration) { if (!this.options.signer) { throw new Error(`Unable to create DID. No signer specified in config.`) } const signature = await this.getRegisterSignature(endpoints); - const response = await this.vdaWeb3Client.register(this.didAddress, endpoints, signature); + let response: any; + if (gasConfig !== undefined) { + response = await this.vdaWeb3Client.register(this.didAddress, endpoints, signature, gasConfig); + } else { + response = await this.vdaWeb3Client.register(this.didAddress, endpoints, signature); + } if (response.success !== true) { - throw new Error('Failed to register endpoints'); + throw new Error(`Failed to register endpoints (${response.error})`); } } @@ -140,7 +145,7 @@ export default class BlockchainApi { * Set a controller of the {@link BlockchainApi#didAddress} to the blockchain * @param controllerPrivateKey private key of new controller */ - public async setController(controllerPrivateKey: string) { + public async setController(controllerPrivateKey: string, gasConfig?: Web3GasConfiguration) { if (!this.options.signer) { throw new Error(`Unable to create DID. No signer specified in config.`) } @@ -148,7 +153,12 @@ export default class BlockchainApi { const controllerAddress = ethers.utils.computeAddress(controllerPrivateKey).toLowerCase(); const signature = await this.getControllerSignature(controllerAddress); - const response = await this.vdaWeb3Client.setController(this.didAddress, controllerAddress, signature); + let response: any; + if (gasConfig !== undefined) { + response = await this.vdaWeb3Client.setController(this.didAddress, controllerAddress, signature, gasConfig); + } else { + response = await this.vdaWeb3Client.setController(this.didAddress, controllerAddress, signature); + } if (response.success !== true) { throw new Error('Failed to set controller'); @@ -187,13 +197,19 @@ export default class BlockchainApi { /** * Revoke a DID address from the blockchain */ - public async revoke() { + public async revoke(gasConfig?: Web3GasConfiguration) { if (!this.options.signer) { throw new Error(`Unable to create DID. No signer specified in config.`) } const signature = await this.getRevokeSignature(); - const response = await this.vdaWeb3Client.revoke(this.didAddress, signature); + let response: any; + if (gasConfig !== undefined) { + response = await this.vdaWeb3Client.revoke(this.didAddress, signature, gasConfig); + } else { + response = await this.vdaWeb3Client.revoke(this.didAddress, signature); + } + if (response.success !== true) { throw new Error('Failed to revoke'); } diff --git a/packages/vda-did/src/blockchain/helpers.ts b/packages/vda-did/src/blockchain/helpers.ts index 4f3b30a2..28b1debb 100644 --- a/packages/vda-did/src/blockchain/helpers.ts +++ b/packages/vda-did/src/blockchain/helpers.ts @@ -1,9 +1,9 @@ -import {ethers} from 'ethers'; +import {BigNumberish, ethers} from 'ethers'; export async function getVeridaSignWithNonce( rawMsg: string, signer: (data: any) => Promise, - nonce: number + nonce: BigNumberish ) { rawMsg = ethers.utils.solidityPack(['bytes', 'uint256'], [rawMsg, nonce]); return signer(rawMsg) diff --git a/packages/vda-did/src/vdaDid.ts b/packages/vda-did/src/vdaDid.ts index 09e172db..e5a33b73 100644 --- a/packages/vda-did/src/vdaDid.ts +++ b/packages/vda-did/src/vdaDid.ts @@ -27,7 +27,7 @@ export default class VdaDid { * @param endpoints * @return VdaDidEndpointResponses Map of endpoints where the DID Document was successfully published */ - public async create(didDocument: DIDDocument, endpoints: string[]): Promise { + public async create(didDocument: DIDDocument, endpoints: string[], retries: number = 3): Promise { this.lastEndpointErrors = undefined if (!this.options.signKey) { throw new Error(`Unable to create DID: No private key specified in config.`) @@ -75,7 +75,9 @@ export default class VdaDid { await this.deleteFromEndpoints(endpoints) // try again - return await this.create(didDocument, endpoints) + if (retries > 0) { + return await this.create(didDocument, endpoints, retries--) + } } // DID already exists, so use update instead diff --git a/packages/vda-did/test/blockchain-api-mainnet-web3.test.ts b/packages/vda-did/test/blockchain-api-mainnet-web3.test.ts new file mode 100644 index 00000000..86af9d96 --- /dev/null +++ b/packages/vda-did/test/blockchain-api-mainnet-web3.test.ts @@ -0,0 +1,84 @@ +const assert = require('assert') +import { DID_LIST } from "@verida/vda-common-test"; +import BlockchainApi from "../src/blockchain/blockchainApi" +import { VdaDidConfigurationOptions } from '@verida/types'; +import { Wallet } from "ethers"; +require('dotenv').config(); + +// const did = Wallet.createRandom(); +const did = new Wallet(DID_LIST[0].privateKey); + +const endPoints_A = ['https://A_1', 'https://A_2', 'https://A_3']; +const endPoints_B = ['https://B_1', 'https://B_2']; +const endPoints_Empty: string[] = []; + + +const privateKey = process.env.PRIVATE_KEY; +if (!privateKey) { + throw new Error('No PRIVATE_KEY in the env file'); +} + +const rpcUrl = 'https://polygon-rpc.com/'; + +const configuration = { + callType: 'web3', + web3Options: { + privateKey, + rpcUrl, + eip1559Mode: 'fast', + eip1559gasStationUrl: 'https://gasstation.polygon.technology/v2' + } + } + +const createBlockchainAPI = (did: any) => { + return new BlockchainApi({ + identifier: did.address, + signKey: did.privateKey, + chainNameOrId: "mainnet", + ...configuration + }) +} + +describe('vda-did blockchain api', () => { + let blockchainApi : BlockchainApi + before(() => { + blockchainApi = createBlockchainAPI(did); + }) + + describe('register', function() { + this.timeout(100 * 1000) + + it('Register successfully', async () => { + await blockchainApi.register(endPoints_A); + + const lookupResult = await blockchainApi.lookup(did.address); + assert.deepEqual( + lookupResult, + {didController: did.address, endpoints: endPoints_A}, + 'Get same endpoints'); + }) + + it('Should update for registered DID', async () => { + await blockchainApi.register(endPoints_B); + + const lookupResult = await blockchainApi.lookup(did.address); + assert.deepEqual( + lookupResult, + {didController: did.address, endpoints: endPoints_B}, + 'Get updated endpoints'); + }) + + it('Should reject for revoked did', async () => { + const tempDID = Wallet.createRandom(); + const testAPI = createBlockchainAPI(tempDID) + + await testAPI.register(endPoints_Empty); + await testAPI.revoke(); + + await assert.rejects( + testAPI.register(endPoints_A), + {message: 'Failed to register endpoints'} + ) + }) + }) +}) \ No newline at end of file diff --git a/packages/vda-did/test/blockchain-api-testnet-gasless.test.ts b/packages/vda-did/test/blockchain-api-testnet-gasless.test.ts new file mode 100644 index 00000000..64b14bfc --- /dev/null +++ b/packages/vda-did/test/blockchain-api-testnet-gasless.test.ts @@ -0,0 +1,179 @@ +const assert = require('assert') +import BlockchainApi from "../src/blockchain/blockchainApi" +import { Wallet } from "ethers"; +import { VdaDidConfigurationOptions, VeridaWeb3ConfigurationOptions, Web3CallType } from '@verida/types'; +require('dotenv').config(); + +const did = Wallet.createRandom(); + +const endPoints_A = ['https://A_1', 'https://A_2', 'https://A_3']; +const endPoints_B = ['https://B_1', 'https://B_2']; +const endPoints_Empty: string[] = []; + +const privateKey = process.env.PRIVATE_KEY; +if (!privateKey) { + throw new Error('No PRIVATE_KEY in the env file'); +} + +const rpcUrl = process.env.RPC_URL; +if (!rpcUrl) { + throw new Error('No RPC_URL in the env file'); +} + +const PORT = process.env.SERVER_PORT ? process.env.SERVER_PORT : 5021; +const SERVER_URL = `http://localhost:${PORT}`; + +const configuration = { + callType: 'gasless', + web3Options: { + serverConfig: { + headers: { + 'context-name': 'Verida Test', + }, + }, + postConfig: { + headers: { + 'user-agent': 'Verida-Vault', + }, + }, + endpointUrl: SERVER_URL, + } + } + +const createBlockchainAPI = (did: any) => { + return new BlockchainApi({ + identifier: did.address, + signKey: did.privateKey, + chainNameOrId: "testnet", + ...configuration + }) +} + +describe('vda-did blockchain api', () => { + let blockchainApi : BlockchainApi + before(() => { + blockchainApi = createBlockchainAPI(did); + }) + + describe('register', function() { + this.timeout(100 * 1000) + + it.only('Register successfully', async () => { + await blockchainApi.register(endPoints_A); + + const lookupResult = await blockchainApi.lookup(did.address); + assert.deepEqual( + lookupResult, + {didController: did.address, endpoints: endPoints_A}, + 'Get same endpoints'); + }) + + it('Should update for registered DID', async () => { + await blockchainApi.register(endPoints_B); + + const lookupResult = await blockchainApi.lookup(did.address); + assert.deepEqual( + lookupResult, + {didController: did.address, endpoints: endPoints_B}, + 'Get updated endpoints'); + }) + + it('Should reject for revoked did', async () => { + const tempDID = Wallet.createRandom(); + const testAPI = createBlockchainAPI(tempDID) + + await testAPI.register(endPoints_Empty); + await testAPI.revoke(); + + await assert.rejects( + testAPI.register(endPoints_A), + {message: 'Failed to register endpoints'} + ) + }) + }) + + describe('Lookup', function() { + this.timeout(100 * 1000) + it('Get endpoints successfully', async () => { + const lookupResult = await blockchainApi.lookup(did.address); + assert.deepEqual( + lookupResult, + {didController:did.address, endpoints:endPoints_B}, + 'Get updated endpoints'); + }) + + it('Should reject for unregistered DID',async () => { + const testDID = Wallet.createRandom(); + const testAPI = createBlockchainAPI(testDID); + await assert.rejects( + testAPI.lookup(testDID.address), + {message: 'DID not found'} + ) + }) + + it('Should reject for revoked DID', async () => { + const testDID = Wallet.createRandom(); + const testAPI = createBlockchainAPI(testDID); + + await testAPI.register(endPoints_A); + await testAPI.revoke(); + + await assert.rejects( + testAPI.lookup(testDID.address), + {message: 'DID not found'} + ) + }) + }) + + describe('Set controller', function() { + this.timeout(100 * 1000) + const controller = Wallet.createRandom(); + it('Should reject for unregistered DID', async () => { + const testAPI = createBlockchainAPI(Wallet.createRandom()); + await assert.rejects( + testAPI.setController(controller.privateKey), + {message: 'Failed to set controller'} + ) + }) + + it('Change controller successfully', async () => { + const orgDID = Wallet.createRandom(); + const testAPI = createBlockchainAPI(orgDID); + + await testAPI.register(endPoints_Empty); + const orgController = await testAPI.getController(); + assert.equal(orgController, orgDID.address, 'Controller itself'); + + await testAPI.setController(controller.privateKey); + const newController = await testAPI.getController(); + assert.equal(newController, controller.address, 'Updated controller'); + + // Restore controller + await testAPI.setController(orgDID.privateKey); + }) + }) + + describe('Revoke', function() { + this.timeout(100 * 1000) + const testAPI = createBlockchainAPI(Wallet.createRandom()); + it('Should reject for unregistered DID', async () => { + await assert.rejects( + testAPI.revoke(), + {message: 'Failed to revoke'} + ); + }) + + it('Revoked successfully', async () => { + await testAPI.register(endPoints_A); + + await testAPI.revoke(); + }) + + it('Should reject for revoked DID', async () => { + await assert.rejects( + testAPI.revoke(), + {message: 'Failed to revoke'} + ); + }) + }) +}) \ No newline at end of file diff --git a/packages/vda-did/test/blockchain-api-testnet-web3-gas-config.test.ts b/packages/vda-did/test/blockchain-api-testnet-web3-gas-config.test.ts new file mode 100644 index 00000000..79c45a44 --- /dev/null +++ b/packages/vda-did/test/blockchain-api-testnet-web3-gas-config.test.ts @@ -0,0 +1,273 @@ +const assert = require('assert') +import BlockchainApi from "../src/blockchain/blockchainApi" +import { BigNumber, Wallet } from "ethers"; +import { VdaDidConfigurationOptions } from '@verida/types'; +require('dotenv').config(); + +const did = Wallet.createRandom(); + +const endPoints_A = ['https://A_1', 'https://A_2', 'https://A_3']; + +const privateKey = process.env.PRIVATE_KEY; +if (!privateKey) { + throw new Error('No PRIVATE_KEY in the env file'); +} + +const rpcUrl = process.env.RPC_URL; +if (!rpcUrl) { + throw new Error('No RPC_URL in the env file'); +} + +const createBlockchainAPI = (did: any, configuration:any) => { + return new BlockchainApi({ + identifier: did.address, + signKey: did.privateKey, + chainNameOrId: "testnet", + ...configuration + }) +} + +const checkResult =async (configuration: any, isSuccess = true, errMsg : string | undefined = undefined) => { + const blockchainApi = createBlockchainAPI(did, configuration); + + if (isSuccess) { + await blockchainApi.register(endPoints_A); + const lookupResult = await blockchainApi.lookup(did.address); + assert.deepEqual( + lookupResult, + {didController: did.address, endpoints: endPoints_A}, + 'Get same endpoints'); + } else { + let f = () => {}; + try { + await blockchainApi.register(endPoints_A); + } catch (err: any) { + f = () => {throw err}; + } finally { + if (errMsg !== undefined) { + assert.throws(f, Error, errMsg); + } else { + assert.throws(f, Error); + } + + } + } +} + +const checkGlobalGasConfig = async (gasOption: Record, isSuccess = true, errMsg : string | undefined = undefined) => { + const configuration = { + callType: 'web3', + web3Options: { + privateKey, + rpcUrl, + ...gasOption + } + } + await checkResult(configuration, isSuccess, errMsg); +} + +const checkMethodDefaultGasConfig = async (gasOption: Record, isSuccess = true, errMsg : string | undefined = undefined) => { + const configuration = { + callType: 'web3', + web3Options: { + privateKey, + rpcUrl, + + methodDefaults: { + "register": gasOption + } + } + } + await checkResult(configuration, isSuccess, errMsg); +} + +const checkRuntimeGasConfig = async (blockchainApi:BlockchainApi, gasOption: Record, isSuccess = true, errMsg : string | undefined = undefined) => { + if (isSuccess) { + await blockchainApi.register(endPoints_A, gasOption); + const lookupResult = await blockchainApi.lookup(did.address); + assert.deepEqual( + lookupResult, + {didController: did.address, endpoints: endPoints_A}, + 'Get same endpoints'); + } else { + let f = () => {}; + try { + await blockchainApi.register(endPoints_A, gasOption); + } catch (err: any) { + f = () => {throw err}; + } finally { + if (errMsg !== undefined) { + assert.throws(f, Error, errMsg); + } else { + assert.throws(f, Error); + } + + } + } +} + +describe('vda-did blockchain api test for different gas configurations', function(){ + let blockchainApi : BlockchainApi + this.timeout(100 * 1000) + + describe('Global gas configuration', function() { + describe('Gas configuration from gas station url', function(){ + it("Failed for insufficient gas parameters",async () => { + let gasOption: Record = { + eip1559Mode: 'fast', + } + await checkGlobalGasConfig(gasOption, false, 'To use the station gas configuration, need to specify eip1559Mode & eip1559gasStationUrl'); + + + gasOption = { + eip1559gasStationUrl: 'https://gasstation-testnet.polygon.technology/v2' + } + await checkGlobalGasConfig(gasOption, false, 'To use the station gas configuration, need to specify eip1559Mode & eip1559gasStationUrl'); + }) + + it("Success", async () => { + const mode = ['safeLow', 'standard', 'fast']; + for (let i = 0; i < mode.length; i++) { + const gasOption = { + eip1559Mode: mode[i], + eip1559gasStationUrl: 'https://gasstation-testnet.polygon.technology/v2' + } + await checkGlobalGasConfig(gasOption, true); + } + }) + }) + + describe('Manual gas configuration test', function() { + it("Success with `maxFeePriority` option only", async () => { + const gasOption = { + maxPriorityFeePerGas: BigNumber.from("1000000000"), //1Gwei + } + await checkGlobalGasConfig(gasOption, true); + }) + + it("Success with `maxFee` option only", async () => { + const gasOption = { + maxFeePerGas: BigNumber.from("5000000000") //5Gwei + } + await checkGlobalGasConfig(gasOption, true); + }) + + it("Success with both parameters",async () => { + const gasOption = { + maxPriorityFeePerGas: BigNumber.from("1000000000"), //1Gwei + maxFeePerGas: BigNumber.from("5000000000") //5Gwei + } + await checkGlobalGasConfig(gasOption, true); + }) + }) + }) + + describe('Method default gas configuration', function() { + describe('Gas configuration from gas station url', function() { + it('Success', async() => { + const gasOption = { + eip1559Mode: 'fast', + eip1559gasStationUrl: 'https://gasstation-testnet.polygon.technology/v2' + } + await checkMethodDefaultGasConfig(gasOption, true); + }) + }) + + describe('Manual gas configuration', function() { + it('Failed for invalid gas configuration', async () => { + const gasOption = { + maxPriorityFeePerGas: BigNumber.from("10000000000"), //10Gwei + maxFeePerGas: BigNumber.from("5000000000") //5Gwei + } + await checkMethodDefaultGasConfig(gasOption, false); + }) + + it('Success', async () => { + const gasOption = { + maxPriorityFeePerGas: BigNumber.from("1000000000"), //1Gwei + maxFeePerGas: BigNumber.from("5000000000") //5Gwei + } + await checkMethodDefaultGasConfig(gasOption, true); + }) + }) + }) + + describe('Gas configuration at runtime', function() { + const blockchainApi = createBlockchainAPI(did, { + callType: 'web3', + web3Options: { + privateKey, + rpcUrl, + } + }) + describe('Gas configuration from gas station url', function() { + it('Success', async () => { + const gasOption = { + eip1559Mode: 'fast', + eip1559gasStationUrl: 'https://gasstation-testnet.polygon.technology/v2' + } + await checkRuntimeGasConfig(blockchainApi, gasOption, true); + }) + }) + + describe('Manual gas configuration', function() { + it('Faild for invalid gas configuration', async () => { + const gasOption = { + maxPriorityFeePerGas: BigNumber.from("10000000000"), //10Gwei + maxFeePerGas: BigNumber.from("5000000000") //5Gwei + } + await checkRuntimeGasConfig(blockchainApi, gasOption, false); + }) + it('Success with valid gas configuration',async () => { + const gasOption = { + maxPriorityFeePerGas: BigNumber.from("1000000000"), //1Gwei + maxFeePerGas: BigNumber.from("5000000000") //5Gwei + } + await checkRuntimeGasConfig(blockchainApi, gasOption, true); + }) + it('Success with empty gas configuration',async () => { + const gasOption = {} + await checkRuntimeGasConfig(blockchainApi, gasOption, true); + }) + }) + + }) + + // describe('register', function() { + // this.timeout(100 * 1000) + + // it.only('Register successfully', async () => { + // await blockchainApi.register(endPoints_A); + + // const lookupResult = await blockchainApi.lookup(did.address); + // assert.deepEqual( + // lookupResult, + // {didController: did.address, endpoints: endPoints_A}, + // 'Get same endpoints'); + // }) + + // it('Should update for registered DID', async () => { + // await blockchainApi.register(endPoints_B); + + // const lookupResult = await blockchainApi.lookup(did.address); + // assert.deepEqual( + // lookupResult, + // {didController: did.address, endpoints: endPoints_B}, + // 'Get updated endpoints'); + // }) + + // it('Should reject for revoked did', async () => { + // const tempDID = Wallet.createRandom(); + // const testAPI = createBlockchainAPI(tempDID) + + // await testAPI.register(endPoints_Empty); + // await testAPI.revoke(); + + // await assert.rejects( + // testAPI.register(endPoints_A), + // {message: 'Failed to register endpoints'} + // ) + // }) + // }) + +}) \ No newline at end of file diff --git a/packages/vda-did/test/blockchain-api.test.ts b/packages/vda-did/test/blockchain-api-testnet-web3.test.ts similarity index 90% rename from packages/vda-did/test/blockchain-api.test.ts rename to packages/vda-did/test/blockchain-api-testnet-web3.test.ts index c39778eb..d871a5b8 100644 --- a/packages/vda-did/test/blockchain-api.test.ts +++ b/packages/vda-did/test/blockchain-api-testnet-web3.test.ts @@ -1,7 +1,7 @@ const assert = require('assert') -import { getBlockchainAPIConfiguration } from "@verida/vda-common-test"; import BlockchainApi from "../src/blockchain/blockchainApi" import { Wallet } from "ethers"; +import { VdaDidConfigurationOptions } from '@verida/types'; require('dotenv').config(); const did = Wallet.createRandom(); @@ -15,9 +15,24 @@ if (!privateKey) { throw new Error('No PRIVATE_KEY in the env file'); } -const configuration = getBlockchainAPIConfiguration(privateKey); +const rpcUrl = process.env.RPC_URL; +if (!rpcUrl) { + throw new Error('No RPC_URL in the env file'); +} + + +const configuration = { + callType: 'web3', + web3Options: { + privateKey, + rpcUrl, + eip1559Mode: 'fast', + eip1559gasStationUrl: 'https://gasstation-testnet.polygon.technology/v2' + } + } + const createBlockchainAPI = (did: any) => { - return new BlockchainApi({ + return new BlockchainApi({ identifier: did.address, signKey: did.privateKey, chainNameOrId: "testnet", @@ -34,7 +49,7 @@ describe('vda-did blockchain api', () => { describe('register', function() { this.timeout(100 * 1000) - it('Register successfully', async () => { + it.only('Register successfully', async () => { await blockchainApi.register(endPoints_A); const lookupResult = await blockchainApi.lookup(did.address); diff --git a/packages/vda-name-client/package.json b/packages/vda-name-client/package.json index d2e13cf5..e0df003d 100644 --- a/packages/vda-name-client/package.json +++ b/packages/vda-name-client/package.json @@ -1,6 +1,6 @@ { "name": "@verida/vda-name-client", - "version": "2.3.6", + "version": "2.4.0-rc5", "description": "Client to manage adding and fetching names from the blockchain", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -25,9 +25,9 @@ }, "dependencies": { "@ethersproject/providers": "^5.7.2", - "@verida/helpers": "^2.3.3", - "@verida/types": "^2.3.1", - "@verida/web3": "^2.3.6", + "@verida/helpers": "^2.4.0-rc2", + "@verida/types": "^2.4.0-rc1", + "@verida/web3": "^2.4.0-rc5", "axios": "^0.27.2", "ethers": "^5.7.0" }, diff --git a/packages/vda-name-client/src/blockchain/blockchainApi.ts b/packages/vda-name-client/src/blockchain/blockchainApi.ts index 022a70d7..7994d25d 100644 --- a/packages/vda-name-client/src/blockchain/blockchainApi.ts +++ b/packages/vda-name-client/src/blockchain/blockchainApi.ts @@ -2,36 +2,15 @@ import { getVeridaContract, VeridaContract } from "@verida/web3" -import { - Web3SelfTransactionConfig, - Web3MetaTransactionConfig, - Web3CallType, - EnvironmentType -} from '@verida/types' +import { Web3SelfTransactionConfig, VdaClientConfig } from '@verida/types' import { ethers, Contract } from "ethers"; import { getContractInfoForNetwork, RPC_URLS, getVeridaSignWithNonce } from "@verida/vda-common"; import { JsonRpcProvider } from '@ethersproject/providers'; import { explodeDID } from '@verida/helpers' -/** - * Interface for vda-name-client instance creation. Same as VDA-DID configuration - * @param did: DID - * @param signKey: private key of DID. Used to generate signature in transactions to chains - * @param chainNameOrId: Target chain name or chain id. - * @param callType : VDA-DID run mode. Values from vda-did-resolver - * @param web3Options: Web3 configuration depending on call type. Values from vda-did-resolver - */ -export interface NameClientConfig { - network: EnvironmentType - did?: string - signKey?: string - callType?: Web3CallType - web3Options?: Web3SelfTransactionConfig | Web3MetaTransactionConfig -} - export class VeridaNameClient { - private config: NameClientConfig + private config: VdaClientConfig private network: string private didAddress?: string @@ -42,7 +21,7 @@ export class VeridaNameClient { // Key = username, Value = DID private usernameCache: Record = {} - public constructor(config: NameClientConfig) { + public constructor(config: VdaClientConfig) { if (!config.callType) { config.callType = 'web3' } diff --git a/packages/vda-sbt-client/CHANGELOG.md b/packages/vda-sbt-client/CHANGELOG.md index d9ea2e3c..cab11e15 100644 --- a/packages/vda-sbt-client/CHANGELOG.md +++ b/packages/vda-sbt-client/CHANGELOG.md @@ -1,3 +1,8 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Update data type of `tokenID` as `BigNumberish` from `number` + 2023-04-27 (v2.3.4) ------------------- diff --git a/packages/vda-sbt-client/package.json b/packages/vda-sbt-client/package.json index 9422cb8a..520815dd 100644 --- a/packages/vda-sbt-client/package.json +++ b/packages/vda-sbt-client/package.json @@ -1,6 +1,6 @@ { "name": "@verida/vda-sbt-client", - "version": "2.3.9", + "version": "2.4.0-rc6.1", "description": "Client to manage adding and fetching names from the blockchain", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -25,15 +25,15 @@ }, "dependencies": { "@ethersproject/providers": "^5.7.2", - "@verida/helpers": "^2.3.3", - "@verida/web3": "^2.3.6", + "@verida/helpers": "^2.4.0-rc2", + "@verida/web3": "^2.4.0-rc5", "axios": "^0.27.2", "ethers": "^5.7.0" }, "devDependencies": { - "@verida/account-node": "^2.3.9", - "@verida/client-ts": "^2.3.9", - "@verida/keyring": "^2.3.3", + "@verida/account-node": "^2.4.0-rc6", + "@verida/client-ts": "^2.4.0-rc6.1", + "@verida/keyring": "^2.4.0-rc1", "@verida/vda-common-test": "^2.3.0", "dotenv": "^16.0.3", "gts": "^3.1.0", diff --git a/packages/vda-sbt-client/src/blockchain/blockchainApi.ts b/packages/vda-sbt-client/src/blockchain/blockchainApi.ts index b0731fae..33043004 100644 --- a/packages/vda-sbt-client/src/blockchain/blockchainApi.ts +++ b/packages/vda-sbt-client/src/blockchain/blockchainApi.ts @@ -2,38 +2,18 @@ import { getVeridaContract, VeridaContract } from "@verida/web3" -import { - Web3SelfTransactionConfig, - Web3MetaTransactionConfig, - Web3CallType, - EnvironmentType -} from '@verida/types' +import { Web3SelfTransactionConfig, VdaClientConfig } from '@verida/types' import { getContractInfoForNetwork, RPC_URLS } from "@verida/vda-common"; import { JsonRpcProvider } from '@ethersproject/providers'; import { explodeDID } from '@verida/helpers' -import { ethers, Contract } from "ethers"; +import { ethers, Contract, BigNumberish } from "ethers"; import EncryptionUtils from "@verida/encryption-utils"; -/** - * Interface for vda-sbt-client instance creation. Same as VDA-DID configuration - * @param did @string DID - * @param signKey @string Private key of DID (hex string). Used to generate signature in transactions to chains - * @param network @string Target chain name or chain id. - * @param callType @string 'web3' | 'gasless' - * @param web3Options object Web3 configuration depending on call type. Same as vda-did-resolver - */ -export interface SBTClientConfig { - network: EnvironmentType - did?: string - signKey?: string - callType?: Web3CallType - web3Options?: Web3SelfTransactionConfig | Web3MetaTransactionConfig -} export class VeridaSBTClient { - private config: SBTClientConfig + private config: VdaClientConfig private network: string private didAddress?: string @@ -41,7 +21,7 @@ export class VeridaSBTClient { private vdaWeb3Client?: VeridaContract private contract?: ethers.Contract - public constructor(config: SBTClientConfig) { + public constructor(config: VdaClientConfig) { if (!config.callType) { config.callType = 'web3' } @@ -101,7 +81,7 @@ export class VeridaSBTClient { * @param tokenId tokenId * @returns tokenURI from SBT contract */ - public async tokenURI(tokenId: number) { + public async tokenURI(tokenId: BigNumberish) { let response try { if (this.vdaWeb3Client) { @@ -127,7 +107,7 @@ export class VeridaSBTClient { * @param tokenId Token ID * @returns true if tokenID is locked */ - public async isLocked(tokenId: number) { + public async isLocked(tokenId: BigNumberish) { let response try { if (this.vdaWeb3Client) { @@ -340,7 +320,7 @@ export class VeridaSBTClient { * @returns string array of SBT type & uniqueId */ public async tokenInfo( - tokenId: number + tokenId: BigNumberish ) { let response try { @@ -369,7 +349,7 @@ export class VeridaSBTClient { * @param tokenId SBT tokenId */ public async burnSBT( - tokenId: number + tokenId: BigNumberish ) { if (this.readOnly || !this.config.signKey) { throw new Error(`Unable to submit to blockchain. In read only mode.`) @@ -388,7 +368,7 @@ export class VeridaSBTClient { * @returns owner address of the token */ public async ownerOf( - tokenId: number + tokenId: BigNumberish ) { let response try { diff --git a/packages/vda-sbt-client/test/blockchain-api.test.ts b/packages/vda-sbt-client/test/blockchain-api.test.ts index 174291e1..0fb0c06b 100644 --- a/packages/vda-sbt-client/test/blockchain-api.test.ts +++ b/packages/vda-sbt-client/test/blockchain-api.test.ts @@ -18,6 +18,7 @@ import { claimSBT, createTestDataIfNotExist } from './utils' +import { BigNumber } from 'ethers'; require('dotenv').config(); const assert = require('assert') @@ -51,9 +52,9 @@ describe('vda-sbt-client blockchain api', () => { describe("tokenURI", function() { this.timeout(60*1000) - let totalSupply : number + let totalSupply : BigNumber; before(async () => { - totalSupply = parseInt(await blockchainApi.totalSupply()); + totalSupply = await blockchainApi.totalSupply(); }) it("Should reject for invalid token IDs",async () => { @@ -62,12 +63,12 @@ describe('vda-sbt-client blockchain api', () => { ) await assert.rejects( - blockchainApi.tokenURI(totalSupply + 1) + blockchainApi.tokenURI(totalSupply.add(1)) ) }) it("Get tokenURI successfully", async () => { - assert.ok(totalSupply > 0, "TotalSupply should be greater than 0") + assert.ok(totalSupply.gt(0), "TotalSupply should be greater than 0") const tokenURI = await blockchainApi.tokenURI(1) assert.ok(tokenURI && tokenURI.length > 0) diff --git a/packages/vda-web3-client/CHANGELOG.md b/packages/vda-web3-client/CHANGELOG.md index 64fe027f..bab79dc1 100644 --- a/packages/vda-web3-client/CHANGELOG.md +++ b/packages/vda-web3-client/CHANGELOG.md @@ -1,3 +1,10 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Fix bugs in `pure` type function call +- Update `callMethod()` function to return BigNumber as BigNumber itself +- Feature: Updated gas configuration in `web3` mode + 2023-04-27 (v2.3.3) ------------------- diff --git a/packages/vda-web3-client/package.json b/packages/vda-web3-client/package.json index 1aabbb7e..9e37397e 100644 --- a/packages/vda-web3-client/package.json +++ b/packages/vda-web3-client/package.json @@ -1,6 +1,6 @@ { "name": "@verida/web3", - "version": "2.3.6", + "version": "2.4.0-rc5", "description": "", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -37,8 +37,8 @@ "typescript": "^4.6.4" }, "dependencies": { - "@verida/types": "^2.3.1", - "@verida/vda-common": "^2.3.6", + "@verida/types": "^2.4.0-rc1", + "@verida/vda-common": "^2.4.0-rc5", "axios": "^1.2.3", "ethers": "^5.7.0" } diff --git a/packages/vda-web3-client/src/VeridaContractBase.ts b/packages/vda-web3-client/src/VeridaContractBase.ts index 328d9903..f7d8f219 100644 --- a/packages/vda-web3-client/src/VeridaContractBase.ts +++ b/packages/vda-web3-client/src/VeridaContractBase.ts @@ -1,9 +1,9 @@ /* eslint-disable prettier/prettier */ import Axios from 'axios'; import { JsonRpcProvider } from '@ethersproject/providers'; -import { isVeridaContract } from './utils' +import { isVeridaContract, getMaticFee } from './utils' import { getContractForNetwork, isVeridaWeb3GasConfiguration } from './config' -import { Wallet, BigNumber, Contract, Signer } from 'ethers'; +import { Wallet, BigNumber, Contract, Signer, utils } from 'ethers'; import { VdaTransactionResult, VeridaWeb3Config, Web3CallType, Web3GasConfiguration, Web3GaslessPostConfig, Web3GaslessRequestConfig, Web3MetaTransactionConfig, Web3SelfTransactionConfig } from '@verida/types'; /** Create axios instance to make http requests to meta-transaction-server */ @@ -23,6 +23,8 @@ export type address = string export type uint256 = BigNumber export type BlockTag = string | number; +type TGasConfigKeys = keyof Web3GasConfiguration; + /** * Class representing any Verida Smart Contrat */ @@ -30,12 +32,6 @@ export class VeridaContract { /** Smart contract interaction mode */ protected type: Web3CallType; - // /** web3 instance. Only used in web3 mode */ - // protected web3?: Web3; - - // /** Account to send transactions in Web3 mode */ - // protected account?: string; - /** Contract instance used in web3 mode */ protected contract?: Contract @@ -98,7 +94,7 @@ export class VeridaContract { if (item.type === 'function') { this[item.name] = async(...params : any[]) : Promise => { - let gasConfig : Web3GasConfiguration | undefined = undefined + let gasConfig : Record | undefined = undefined const paramLen = params.length if (params !== undefined @@ -112,22 +108,46 @@ export class VeridaContract { } else if (this.web3Config?.methodDefaults !== undefined && (item.name in this.web3Config?.methodDefaults)) { // Use gas configuration in the methodDefaults gasConfig = Object.assign({}, this.web3Config.methodDefaults[item.name]) - } else if (this.web3Config !== undefined) { + } else if (this.web3Config !== undefined && isVeridaWeb3GasConfiguration(this.web3Config)) { // Use gas configuration in the global configuration + const keys:TGasConfigKeys[] = ['eip1559Mode', 'eip1559gasStationUrl', 'maxFeePerGas', 'maxPriorityFeePerGas', 'gasLimit', 'gasPrice']; gasConfig = {} - if ('maxFeePerGas' in this.web3Config) { - gasConfig['maxFeePerGas'] = this.web3Config['maxFeePerGas'] + for (let i = 0; i < keys.length; i++) { + if (keys[i] in this.web3Config) { + gasConfig[keys[i]] = this.web3Config[keys[i]]; + } } + } - if ('maxPriorityFeePerGas' in this.web3Config) { - gasConfig['maxPriorityFeePerGas'] = this.web3Config['maxPriorityFeePerGas'] - } + // console.log('vda-web3 gasconfig : ', gasConfig); + if (gasConfig === undefined || Object.keys(gasConfig).length === 0) { + // Call transaction without gas configuration + return this.callMethod(item.name, item.stateMutability, params) + } - if ('gasLimit' in this.web3Config) { - gasConfig['gasLimit'] = this.web3Config['gasLimit'] + const eip1559Keys:TGasConfigKeys[] = ['eip1559Mode', 'eip1559gasStationUrl']; + const keys:TGasConfigKeys[] = ['maxFeePerGas', 'maxPriorityFeePerGas', 'gasLimit', 'gasPrice']; + let isGasConfigured = false; + for (let i = 0; i < keys.length; i++) { + if (keys[i] in gasConfig) { + isGasConfigured = true; + break; + } + } + if (isGasConfigured) { + // Remove unnecessary EIP1559 keys if exist in the gas config + for (let i = 0; i < eip1559Keys.length; i++) { + if (eip1559Keys[i] in gasConfig) { + delete gasConfig[eip1559Keys[i]]; + } + } + } else { // Need to pull the gas configuration from the station + if ('eip1559Mode' in gasConfig && 'eip1559gasStationUrl' in gasConfig) { + gasConfig = await getMaticFee(gasConfig['eip1559gasStationUrl'], gasConfig['eip1559Mode']); + } else { + throw new Error('To use the station gas configuration, need to specify eip1559Mode & eip1559gasStationUrl'); } } - return this.callMethod(item.name, item.stateMutability, params, gasConfig) } } @@ -222,33 +242,36 @@ export class VeridaContract { let ret; const contract = await this.attachContract() - // console.log('Contract = ', contract) try { - if (methodType === 'view') { + if (methodType === 'view' || methodType === 'pure') { ret = await contract.callStatic[methodName](...params) } else { - // console.log('Gas Config : ', gasConfig) - let transaction: any - if (gasConfig === undefined) { //Gas configuration is in the params - transaction = await contract.functions[methodName](...params) - } else { // Need to use manual gas configuration - transaction = await contract.functions[methodName](...params, gasConfig) + + if (gasConfig === undefined || Object.keys(gasConfig).length === 0) { //No gas configuration + transaction = await contract.functions[methodName](...params); + } else { + transaction = await contract.functions[methodName](...params, gasConfig); } - const transactionReceipt = await transaction.wait(1) - // console.log('Transaction Receipt = ', transactionRecipt) + // console.log("transaction : ", transaction); + const transactionReceipt = await transaction.wait() + // console.log("transactionReceipt : ", transactionReceipt); ret = transactionReceipt } } catch(e: any) { - // console.log('Error in transaction', e) + // console.log('vda-web3 : Error in transaction', e) let reason = e.reason ? e.reason : 'Unknown' reason = e.error && e.error.reason ? e.error.reason : reason reason = reason.replace('execution reverted: ','') + if (reason === 'Unknown' && e.errorName) { + reason = e.errorName; + } + return Promise.resolve({ success: false, error: e.toString(), @@ -257,7 +280,8 @@ export class VeridaContract { }) } - if (BigNumber.isBigNumber(ret)) ret = ret.toNumber() + // Overflow error in `vda-node-manager` to get node issue fee. + // if (BigNumber.isBigNumber(ret)) ret = ret.toNumber() return { success: true, diff --git a/packages/vda-web3-client/src/config.ts b/packages/vda-web3-client/src/config.ts index 9f21eb90..3658fe94 100644 --- a/packages/vda-web3-client/src/config.ts +++ b/packages/vda-web3-client/src/config.ts @@ -3,12 +3,15 @@ import { BigNumber } from '@ethersproject/bignumber' import { Contract } from '@ethersproject/contracts' import { JsonRpcProvider, Provider } from '@ethersproject/providers' import { knownNetworks } from './constants' -import { Web3ContractInfo, Web3ProviderConfiguration } from '@verida/types' +import { Web3ContractInfo, Web3ProviderConfiguration, Web3GasConfiguration } from '@verida/types' export function isVeridaWeb3GasConfiguration(obj : Object) { return ('maxFeePerGas' in obj) || ('maxPriorityFeePerGas' in obj) || ('gasLimit' in obj) + || ('gasPrice' in obj) + || ('eip1559Mode' in obj) + || ('eip1559gasStationUrl' in obj); } diff --git a/packages/vda-web3-client/src/utils.ts b/packages/vda-web3-client/src/utils.ts index 32bb0d46..36548773 100644 --- a/packages/vda-web3-client/src/utils.ts +++ b/packages/vda-web3-client/src/utils.ts @@ -1,6 +1,9 @@ /* eslint-disable prettier/prettier */ -import { veridaContractWhiteList } from './constants' +import { veridaContractWhiteList } from './constants'; +import { ethers } from 'ethers'; +import { providers } from 'ethers'; +import Axios from 'axios'; /** * Convert string to 32Bytes value @@ -20,3 +23,86 @@ export function stringToBytes32(str: string): string { export function isVeridaContract(contractAddress: string) : boolean { return veridaContractWhiteList.includes(contractAddress.toLowerCase()) } + +/** + * Get Polygon fee data to send the transactions + * @param gasStationUrl Gas station url to pull the gas inforamtion + * @returns Matic fee data + */ +export async function getMaticFee(gasStationUrl: string, mode: string) { + let maxFeePerGas = ethers.BigNumber.from(40000000000); // fallback to 40 gwei + let maxPriorityFeePerGas = ethers.BigNumber.from(40000000000); // fallback to 40 gwei + // const gasLimit = ethers.BigNumber.from(50000000000); // fallback to 50 gwei + + try { + const { data } = await Axios({ + method: 'get', + url: gasStationUrl + }); + + maxFeePerGas = ethers.utils.parseUnits( + Math.ceil(data[mode].maxFee) + '', + 'gwei' + ); + maxPriorityFeePerGas = ethers.utils.parseUnits( + Math.ceil(data[mode].maxPriorityFee) + '', + 'gwei' + ); + } catch { + // ignore + console.log('Error in get gasfee from gas station url'); + } + + // return { maxFeePerGas, maxPriorityFeePerGas, gasLimit }; + return { maxFeePerGas, maxPriorityFeePerGas }; +} + +export async function checkEIP1559Enabled(provider?: providers.Provider, chainUrl?:string) : Promise { + if (provider === undefined) { + if (chainUrl === undefined) { + return false; + } + + provider = new ethers.providers.JsonRpcProvider(chainUrl); + } + const blockNumber = await provider.getBlockNumber(); + const block = await provider.getBlock(blockNumber); + + return block.baseFeePerGas !== undefined; +} + + +// export async function getMaticFee(isProd: boolean) { +// let maxFeePerGas = ethers.BigNumber.from(40000000000); // fallback to 40 gwei +// let maxPriorityFeePerGas = ethers.BigNumber.from(40000000000); // fallback to 40 gwei +// const gasLimit = ethers.BigNumber.from(50000000000); // fallback to 50 gwei + +// const EXTRA_TIP_FOR_MINER = 100 // gwei + +// try { +// const { data } = await Axios({ +// method: 'get', +// url: isProd +// ? 'https://gasstation-mainnet.matic.network/v2' +// : 'https://gasstation-mumbai.matic.today/v2', +// }); + +// console.log('//////////////', data); + +// const base_fee = parseFloat(data.estimatedBaseFee) +// const max_priority_fee = data.fast.maxPriorityFee + EXTRA_TIP_FOR_MINER; +// let max_fee_per_gas = base_fee + max_priority_fee + +// // In case the network gets (up to 25%) more congested +// max_fee_per_gas += (base_fee * 0.15) + +// // cast gwei numbers to wei BigNumbers for ethers +// maxFeePerGas = ethers.utils.parseUnits(max_fee_per_gas.toFixed(9), 'gwei') +// maxPriorityFeePerGas = ethers.utils.parseUnits(max_priority_fee.toFixed(9), 'gwei') +// } catch { +// // ignore +// console.log('Error in get gasfee'); +// } + +// return { maxFeePerGas, maxPriorityFeePerGas }; +// } diff --git a/packages/verifiable-credentials/package.json b/packages/verifiable-credentials/package.json index 6595847d..63e3c23c 100644 --- a/packages/verifiable-credentials/package.json +++ b/packages/verifiable-credentials/package.json @@ -1,6 +1,6 @@ { "name": "@verida/verifiable-credentials", - "version": "2.3.9", + "version": "2.4.0-rc6.1", "main": "dist/index.js", "license": "ISC", "directories": { @@ -17,10 +17,10 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/encryption-utils": "^2.2.3", - "@verida/helpers": "^2.3.3", - "@verida/types": "^2.3.1", - "@verida/vda-did-resolver": "^2.3.9", + "@verida/encryption-utils": "^2.2.2", + "@verida/helpers": "^2.4.0-rc2", + "@verida/types": "^2.4.0-rc1", + "@verida/vda-did-resolver": "^2.4.0-rc5", "axios": "^1.3.3", "dayjs": "^1.10.7", "did-jwt": "^6.11.0", @@ -31,7 +31,7 @@ }, "devDependencies": { "@types/lodash": "^4.14.177", - "@verida/client-ts": "^2.3.9", + "@verida/client-ts": "^2.4.0-rc6.1", "dotenv": "^16.0.3", "mocha": "^8.2.1", "ts-mocha": "^8.0.0", diff --git a/packages/web-helpers/CHANGELOG.md b/packages/web-helpers/CHANGELOG.md index 2832dc0c..82734b7b 100644 --- a/packages/web-helpers/CHANGELOG.md +++ b/packages/web-helpers/CHANGELOG.md @@ -1,3 +1,8 @@ +2023-12-26 (v.2.4.0) +------------------- + +- Fix: Unable to re-open modal after closing (`WebUser.connecting` wasn't being reset) + 2023-08-22 (v3.0.2) ------------------- diff --git a/packages/web-helpers/package.json b/packages/web-helpers/package.json index 8a9d1dde..cbe51611 100644 --- a/packages/web-helpers/package.json +++ b/packages/web-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@verida/web-helpers", - "version": "3.0.2", + "version": "2.4.0-rc6.1", "main": "dist/index.js", "license": "ISC", "directories": { @@ -15,9 +15,9 @@ "node": ">=14.0.0" }, "dependencies": { - "@verida/account-web-vault": "^2.3.9", - "@verida/client-ts": "^2.3.9", - "@verida/types": "^2.3.1" + "@verida/account-web-vault": "^2.4.0-rc6", + "@verida/client-ts": "^2.4.0-rc6.1", + "@verida/types": "^2.4.0-rc1" }, "devDependencies": { "mocha": "^8.2.1", diff --git a/packages/web-helpers/src/WebUser.ts b/packages/web-helpers/src/WebUser.ts index e44f1f84..2c783fcf 100644 --- a/packages/web-helpers/src/WebUser.ts +++ b/packages/web-helpers/src/WebUser.ts @@ -193,6 +193,10 @@ export class WebUser extends EventEmitter { * @returns A Promise that will resolve to true / false depending on if the user is connected */ public async connect() { + if (this.isConnected()) { + return true + } + if (this.connecting) { // Have an existing promise (that may or may not be resolved) // Return it so if it's pending, the requestor will wait @@ -202,6 +206,7 @@ export class WebUser extends EventEmitter { // Create a promise that will connect to the network and resolve once complete // Also pre-populates the user's public profile const config = this.config + const webUser = this this.connecting = new Promise(async (resolve, reject) => { const account = new VaultAccount(config.accountConfig); @@ -216,6 +221,7 @@ export class WebUser extends EventEmitter { console.log('User cancelled login attempt by closing the QR code modal or an unexpected error occurred'); } + webUser.connecting = undefined resolve(false) return } @@ -231,6 +237,7 @@ export class WebUser extends EventEmitter { const profile = await this.getPublicProfile() this.emit('connected', profile) + webUser.connecting = undefined resolve(true) }) diff --git a/yarn.lock b/yarn.lock index 66795de2..e9c8426e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2720,6 +2720,15 @@ axios@^1.3.3: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" + integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"