diff --git a/package.json b/package.json index 913a4fd0d..d863eba48 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@verida/client-rn": "^2.2.0", "@verida/encryption-utils": "^2.2.0", "@verida/types": "^2.2.0", + "@verida/vda-sbt-client": "^2.2.1", "@verida/verifiable-credentials": "^2.2.0", "@verida/wallet-utils": "^1.7.4", "@walletconnect/client": "^1.7.7", diff --git a/src/api/DataConnectorsManager.ts b/src/api/DataConnectorsManager.ts index 70a301b92..1a4c8d1e5 100644 --- a/src/api/DataConnectorsManager.ts +++ b/src/api/DataConnectorsManager.ts @@ -6,6 +6,7 @@ import { Linking } from 'react-native' import CONFIG from '../config/environment' import AccountManager from './AccountManager' +import { SBTManager } from './SBTManager' const DATA_CONNECTION_SCHEMA = 'https://vault.schemas.verida.io/data-connections/connection/v0.1.0/schema.json' @@ -20,24 +21,9 @@ const delay = async (ms: number) => { await new Promise((resolve: any) => setTimeout(() => resolve(), ms)) } -// possible states for status: syncing, disabled, active +let CONNECTION_CACHE: any -// @todo: Pull this from the server -const FacebookIcon = require('assets/social_icons/facebook.png') -const TwitterIcon = require('assets/social_icons/twitter.png') - -const CONNECTIONS: any = { - facebook: { - name: 'facebook', - label: 'Facebook', - icon: FacebookIcon, - }, - twitter: { - name: 'twitter', - label: 'Twitter', - icon: TwitterIcon, - }, -} +// possible states for status: syncing, disabled, active class DataConnectorsEvents extends EventEmitter { private static instance: DataConnectorsEvents @@ -85,7 +71,19 @@ export default class DataConnectorsManager { return DataConnectorsManager.datastore } - static getConnectionInfo(connectorName: string) { + static async getConnections(): Promise> { + if (CONNECTION_CACHE) { + return CONNECTION_CACHE + } + + // @todo cache + const response = await axios.get(`${CONFIG.DATA_CONNECTOR_URL}/providers`) + CONNECTION_CACHE = response.data + return CONNECTION_CACHE + } + + static async getConnectionInfo(connectorName: string) { + const CONNECTIONS = await DataConnectorsManager.getConnections() return CONNECTIONS[connectorName] } @@ -94,7 +92,15 @@ export default class DataConnectorsManager { return DataConnectorsManager._connections[connectorName] } - const connector = new DataConnection(connectorName) + const connectionInfo = await DataConnectorsManager.getConnectionInfo( + connectorName + ) + + const connector = new DataConnection( + connectorName, + connectionInfo.icon, + connectionInfo.label + ) await connector.init() DataConnectorsManager._connections[connectorName] = connector @@ -102,7 +108,9 @@ export default class DataConnectorsManager { } static async getConnectors(): Promise { - const connections: any = Object.values(CONNECTIONS) + const connections: any = Object.values( + await DataConnectorsManager.getConnections() + ) const connectors: any = {} for (let i = 0; i < connections.length; i++) { const connection = await DataConnectorsManager.getConnection( @@ -115,7 +123,9 @@ export default class DataConnectorsManager { } static async resetConnector() { - const connections: any = Object.values(CONNECTIONS) + const connections: any = Object.values( + await DataConnectorsManager.getConnections() + ) for (let i = 0; i < connections.length; i++) { if ( DataConnectorsManager._connections[connections[i].name].syncStatus !== @@ -174,12 +184,12 @@ class DataConnection extends EventEmitter { public metadata?: any public icon?: any - constructor(name: string) { + constructor(name: string, icon: string, label: string) { super() this.name = this.source = name this.syncStatus = 'disabled' - this.icon = CONNECTIONS[this.name].icon - this.label = CONNECTIONS[this.name].label + this.icon = icon + this.label = label this.syncFrequency = 'hour' } @@ -369,9 +379,20 @@ class DataConnection extends EventEmitter { const syncRequest = await externalDatastore.get(syncRequestId) if (syncRequest.status === 'complete') { - // Sync has completed on the server, so complete the sync - // by replicating data from the server - this.syncReplication(serverDid, contextName, syncRequest) + // Sync has completed on the server + + // Save a SBT credential if it was provided + if (syncRequest.syncInfo.profile.credential) { + const sbtManager = new SBTManager() + const profileCredentialId = `${syncRequest.source}-${syncRequest.syncInfo.profile.id}-profile` + await sbtManager.saveCredential( + profileCredentialId, + syncRequest.syncInfo.profile.credential + ) + } + + // Complete the sync by replicating data from the server + await this.syncReplication(serverDid, contextName, syncRequest) } else { if (retryCount === 0) { // Retry count limit hit diff --git a/src/api/SBTManager.ts b/src/api/SBTManager.ts new file mode 100644 index 000000000..7c778e9db --- /dev/null +++ b/src/api/SBTManager.ts @@ -0,0 +1,347 @@ +import EncryptionUtils from '@verida/encryption-utils' +import { + buildVeridaUri, + explodeDID, + explodeVeridaUri, + wrapUri, +} from '@verida/helpers' +import { + DatabasePermissionOptionsEnum, + EnvironmentType, + IContext, + IDatastore, + Web3CallType, +} from '@verida/types' +import { VeridaSBTClient } from '@verida/vda-sbt-client' +import { Credentials } from '@verida/verifiable-credentials' +import _ from 'lodash' + +import CONFIG from '../config/environment' +import AccountManager from './AccountManager' +import { Account } from './types' + +const SCHEMA_SBT = + 'https://common.schemas.verida.io/token/sbt/storage/v0.1.0/schema.json' +const SCHEMA_CREDENTIALS = + 'https://common.schemas.verida.io/credential/base/v0.2.0/schema.json' + +export class SBTManager { + private client?: VeridaSBTClient + + /** + * Save a SBT credential + * + * @param id + * @param credential + * @param forceUpdate + */ + public async saveCredential(id: string, credential: any) { + console.log('saveCredential()') + const context = ( + await AccountManager.getInstance().getVeridaContext() + ) + + // Open the datastore + const datastore = await context.openDatastore(SCHEMA_CREDENTIALS, {}) + const credentialRecord = { + _id: id, + ...credential, + } + + // Try to fetch an existing record + let existingRecord + try { + existingRecord = await datastore.get(id, {}) + + // Record exists, + const { _rev } = existingRecord + + if ( + !_.isEqual( + existingRecord.credentialData, + credentialRecord.credentialData + ) + ) { + // Data has changed, need to update + // Set the existing record revision so update process correctly + credentialRecord._rev = _rev + + // Data that changed + //const changes = _.differenceWith(_.toPairs(existingRecord.credentialData), _.toPairs(credentialRecord.credentialData), _.isEqual) + //console.log(changes) + + // Save the credential record + if (!(await datastore.save(credentialRecord, {}))) { + console.log('Invalid SBT credential', datastore.errors) + } + } + } catch (err) { + // Record doesn't exist, create it + await datastore.save(credentialRecord, { + forceInsert: true, + }) + } + } + + public async isMinted(credentialRecord: any) { + console.log('isMinted?', credentialRecord) + const client = await this.getClient() + console.log(credentialRecord.credentialData.did.toLowerCase()) + + try { + const claimedSbts = await client.getClaimedSBTList( + credentialRecord.credentialData.did.toLowerCase() + ) + console.log(claimedSbts) + } catch (err) { + console.log(err.message) + } + } + + public async burnSbt( + credentialRecord: any, + mintAddress: string + ): Promise { + console.log('burnSbt', mintAddress) + mintAddress = mintAddress.toLowerCase() + const context = ( + await AccountManager.getInstance().getVeridaContext() + ) + + // Open the datastore + const datastore = await context.openDatastore(SCHEMA_SBT, { + permissions: { + read: DatabasePermissionOptionsEnum.PUBLIC, + write: DatabasePermissionOptionsEnum.OWNER, + }, + }) + + // Get the minted SBT + const sbtId = `${mintAddress}-${credentialRecord._id}` + let sbtRecord + try { + sbtRecord = await datastore.get(sbtId, {}) + } catch (err) { + // doesn't exist + throw new Error(`SBT hasn't been minted`) + } + + // Delete the minted SBT from the public database + //await datastore.delete(sbtId) + + // Unmint the SBT + const client = await this.getClient() + + /*try { + const claimedSbts = await client.getClaimedSBTList(mintAddress.toLowerCase()) + console.log(claimedSbts) + } catch (err) { + console.log(err.message) + }*/ + try { + const response = await client.burnSBT(61) + console.log('burnt', response) + } catch (err) { + console.log('burn error') + console.log(err.message) + return false + } + } + + /** + * Mint a SBT + * + * @param credentialRecord + * @param mintAddress + */ + public async mintSbt( + credentialRecord: any, + mintAddress: string + ): Promise { + console.log('mintSbt', mintAddress) + mintAddress = mintAddress.toLowerCase() + // @todo: check it hasn't been minted already + //console.log(credentialRecord, mintAddress) + //return + + const context = ( + await AccountManager.getInstance().getVeridaContext() + ) + + // Open the datastore + const datastore = await context.openDatastore(SCHEMA_SBT, { + permissions: { + read: DatabasePermissionOptionsEnum.PUBLIC, + write: DatabasePermissionOptionsEnum.OWNER, + }, + }) + + // Check this SBT hasn't already been minted + const sbtId = `${mintAddress}-${credentialRecord._id}` + try { + await datastore.get(sbtId, {}) + console.log('already exists', sbtId) + // exists + return false + } catch (err) { + // doesn't exist + } + + // Save this SBT + const sbtData: any = { + _id: sbtId, + ...credentialRecord.credentialData, + didJwtVc: credentialRecord.didJwtVc, + } + + console.log('sbtData', sbtData) + + const result: any = await datastore.save(sbtData, { + forceInsert: true, + }) + console.log(result) + console.log(datastore.errors) + const db = await datastore.getDb() + const info = await db.info() + const credentialUri = buildVeridaUri( + await context.getAccount().did(), + context.getContextName(), + info.databaseName, + sbtId, + [] + ) + console.log(credentialUri) + + //const credentialUri = 'verida://did:vda:testnet:0xcD61d79C7db8fF5F80feCacEc0aE57274F5D6dF5/Verida%20Testing:%20Fake%20Vault/token_metadata_public/5d99c6e0-d38a-11ed-8135-f5f9ab7f39c3' + + // Fetch credential record from the network + /*const credentialRecord = await fetchVeridaUri( + credentialUri, + context.getClient() + )*/ + + // Generate URL to mint that generates the metadata + const sbtUri = wrapUri(credentialUri, 'https://data.verida.network') + '.json' + console.log('sbtUri', sbtUri) + + const credentials = new Credentials() + + //const sbtClient = await SbtController.getSbtClient() + const generatedCredential = await credentials.verifyCredential( + credentialRecord.didJwtVc, + {} + ) + //const sbtData = generatedCredential.verifiableCredential.credentialSubject + const proofs = generatedCredential.payload.vc.proofs + const vcIssuerDid = generatedCredential.payload.iss + + // Get the context proof of the issuer + // (Links their DID to the signing key of the context that signed the credential proof) + // @ts-ignore + const didClient = context.getClient().didClient + const issuerDidDoc = await didClient.get(vcIssuerDid) + const issuerContextProof = issuerDidDoc.locateContextProof( + generatedCredential.payload.vc.veridaContextName + ) + + console.log('issuerDid', vcIssuerDid) + console.log('issuerContextProof', issuerContextProof) + const { did } = explodeVeridaUri(credentialUri) + console.log('subject did', did) + const { address } = explodeDID(did) + const proofString = `${sbtData.type}-${ + sbtData.uniqueAttribute + }-${address.toLowerCase()}` + console.log('proof string', proofString) + console.log(proofs['type-unique-didAddress']) + + const signingAddress = EncryptionUtils.getSigner( + proofString, + proofs['type-unique-didAddress'] + ) + console.log('address that signed SBT string (issuer)', signingAddress) + + /* + const issuerDidAddress = '0xB3d245bC0Fa8479b1B0b200c26f8c93e4737efC3' + const requestProofMsg = `${issuerDidAddress}${signingAddress}`.toLowerCase() + const privateKeyArray = new Uint8Array( + Buffer.from(serverconfig.testing.veridaPrivateKey.slice(2), 'hex') + )*/ + //const testSign = EncryptionUtils.signData(requestProofMsg, privateKeyArray) + /*const signerContextSigner = EncryptionUtils.getSigner( + requestProofMsg, + issuerContextProof + ) + console.log( + 'requestProofMsg (issuer signing context text)', + requestProofMsg + ) + console.log( + 'signerContextSigner (issuer signing context proof)', + signerContextSigner + )*/ + /*console.log('test sign', testSign) + + console.log('these two should match') + console.log(testSign, issuerContextProof) + + const keyring = await connection.account.keyring(generatedCredential.payload.vc.veridaContextName) + + // Get keyring keys so public keys and ownership proof can be saved to the DID document + const keys = await keyring.getKeys() + console.log(keys) + + // Generate a proof that the DID controls the context public signing key that can be used on chain + const proofStringReal = `${issuerDidAddress}${keys.signPublicAddress}`.toLowerCase() + console.log('real proof string', proofStringReal) + + const signer2 = EncryptionUtils.getSigner(proofStringReal, issuerContextProof) + console.log('signer2', signer2)*/ + + //return + + // Initiate a SBT claim on-chain + + try { + const client = await this.getClient() + const mintResult = await client.claimSBT( + sbtData.type, + sbtData.uniqueAttribute, + sbtUri, + mintAddress, + proofs['type-unique-didAddress'], + issuerContextProof + ) + + console.log('mint result') + console.log(mintResult) + return true + } catch (err) { + console.log('mint error!') + console.log(err.message) + console.log(err.reason) + } + } + + private async getClient() { + if (this.client) { + return this.client + } + + const didClientConfig = CONFIG.VERIDA_DID_CLIENT_CONFIG + const account = ( + await AccountManager.getInstance().getSelectedAccount() + ) + + const sbtClient = new VeridaSBTClient({ + callType: didClientConfig.callType, + did: account.did, + signKey: account.privateKey, + network: CONFIG.ENVIRONMENT, + web3Options: didClientConfig.web3Config, + }) + + this.client = sbtClient + return this.client + } +} diff --git a/src/api/VaultCommon/managers/credentials.ts b/src/api/VaultCommon/managers/credentials.ts deleted file mode 100644 index 15cb1bfd4..000000000 --- a/src/api/VaultCommon/managers/credentials.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*import { - VeridaApp, - Database -} from "../interfaces/VeridaApp"; - -const CREDENTIAL_DB = 'credential' - -export class CredentialsManager { - - _app: VeridaApp - _db?: Database - - constructor (app: VeridaApp) { - this._app = app - } - - // @todo - async get(credentialId: string, options: object) { - await this._init() - return this._db?.get(credentialId, options) - } - - // @todo - async getMany(filter: object, options: object) { - await this._init() - return this._db?.getMany(filter, options) - } - - async _init() { - if (this._db) { - return - } - - this._db = await this._app.openDatabase(CREDENTIAL_DB) - } - -} -*/ \ No newline at end of file diff --git a/src/api/VaultCommon/managers/login.ts b/src/api/VaultCommon/managers/login.ts deleted file mode 100644 index ea6531a6e..000000000 --- a/src/api/VaultCommon/managers/login.ts +++ /dev/null @@ -1,25 +0,0 @@ - -/** - * Manage login requests and responses - */ -export class LoginManager { - - _app: any - - constructor (app: any) { - this._app = app - } - - /** - * Get a list of all login requests - */ - async getMany(filter: object, options: object) {} - - /** - * Get a specific login request - * - * @param requestId - */ - async get(requestId: string) {} - -} \ No newline at end of file diff --git a/src/hooks/useDeeplink.ts b/src/hooks/useDeeplink.ts index 204468f70..16a57e861 100644 --- a/src/hooks/useDeeplink.ts +++ b/src/hooks/useDeeplink.ts @@ -5,6 +5,7 @@ import * as Sentry from '@sentry/react-native' import parse from 'url-parse' import { DashboardTabParams, MainStackParams } from 'navigation/types' +import DataConnectorsManager from 'api/DataConnectorsManager' type NavProp = CompositeNavigationProp< BottomTabNavigationProp, @@ -12,7 +13,7 @@ type NavProp = CompositeNavigationProp< > export function useDeeplink(navigation: NavProp) { - return function (url: string) { + return async function (url: string) { try { const parsedUrl = parse(url, true) const { pathname, query } = parsedUrl @@ -29,6 +30,8 @@ export function useDeeplink(navigation: NavProp) { if (screenName === 'SingleConnection') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore need to better typing here + query.provider = await DataConnectorsManager.getConnectionInfo(query.provider) + navigation.jumpTo('Connections') } navigation.navigate(screenName, query as never) diff --git a/src/pages/Assets/Collectibles.tsx b/src/pages/Assets/Collectibles.tsx index e6bd25328..c6cf89bbc 100644 --- a/src/pages/Assets/Collectibles.tsx +++ b/src/pages/Assets/Collectibles.tsx @@ -39,8 +39,9 @@ import { Theme } from 'styles/types' import { IMAGE_WIDTH, NUMBER_OF_COLUMNS } from './constants' const caipNormalizeAddress = (address: string) => { - // FIXME: hardcode just ethereum for now - return `eip155:5:${address}` + // FIXME: hardcode just mumbai for now + // was 5 + return `eip155:80001:${address}` } const Collectibles = () => { diff --git a/src/pages/Connections/DataConnector.js b/src/pages/Connections/DataConnector.js index db472bed1..91e5e1b21 100644 --- a/src/pages/Connections/DataConnector.js +++ b/src/pages/Connections/DataConnector.js @@ -64,12 +64,12 @@ export default (props) => { { props.navigation.navigate('SingleConnection', { - provider: item.name, + provider: item, }) }} style={styles.connectionItem}> - + {item.label} {item.syncStatus} diff --git a/src/pages/Connections/SingleConnection.js b/src/pages/Connections/SingleConnection.js index 7ba549373..1fc6680c0 100644 --- a/src/pages/Connections/SingleConnection.js +++ b/src/pages/Connections/SingleConnection.js @@ -22,8 +22,8 @@ const calculateNextSync = function (conn) { } export default ({ route, navigation }) => { - const provider = route.params.provider - const connectionInfo = DataConnectorsManager.getConnectionInfo(provider) + const connectionInfo = route.params.provider + const provider = connectionInfo.name const [syncStatus, setSyncStatus] = useState('') const [nextSync, setNextSync] = useState('') @@ -105,7 +105,10 @@ export default ({ route, navigation }) => { )} - + {syncStatus === 'disabled' ? ( + + + {!loading && verified && ( diff --git a/yarn.lock b/yarn.lock index 9c2341c47..4831ee2e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4041,6 +4041,16 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" +"@verida/encryption-utils@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@verida/encryption-utils/-/encryption-utils-2.2.1.tgz#76104228a194187f8019bf97e6b609d6742c67a3" + integrity sha512-0UNlTd9In2JJD8Ov5RXX9t4ta52gtpwJvg00nR4oT8TpMH7B2SEaLzJZqvBK3cRb3Y5IMJxdmjDhiParj9a+zw== + dependencies: + ethers "^5.5.1" + json.sortify "^2.2.2" + tweetnacl "^1.0.3" + tweetnacl-util "^0.15.1" + "@verida/helpers@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@verida/helpers/-/helpers-2.2.0.tgz#1de0e0b79dbaa59ed3623535192de456e9f1308a" @@ -4051,6 +4061,16 @@ bs58 "^5.0.0" url "^0.11.0" +"@verida/helpers@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@verida/helpers/-/helpers-2.2.1.tgz#f3ea2b183e133d9f96c052b7b65da694bfed8c2f" + integrity sha512-Ozv9UTgHs3aLPOB1D7Cg/dvDl1GzAtSkGrP+eddUkZHpI5tp2b6W4McUd7sWDdERAmPAuKbeWeAZdnMXgRSlSQ== + dependencies: + "@verida/encryption-utils" "^2.2.1" + "@verida/types" "^2.2.0" + bs58 "^5.0.0" + url "^0.11.0" + "@verida/keyring@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@verida/keyring/-/keyring-2.2.0.tgz#c7f7ace4b47ec8bceb626210b2f38d20b4f1e360" @@ -4127,6 +4147,17 @@ axios "^0.27.2" ethers "^5.7.0" +"@verida/vda-sbt-client@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@verida/vda-sbt-client/-/vda-sbt-client-2.2.1.tgz#9b5b2bda2705694f739034a5f7859593c6f396f1" + integrity sha512-SrqdM0HEnxzAybVpn9yyy0WL1ZSIvoPxwTmBOkrlPb/86/bsZIYiC1Iuae2g8XsdCu06S1I+gr/Qu8EzdyBTrw== + dependencies: + "@ethersproject/providers" "^5.7.2" + "@verida/helpers" "^2.2.1" + "@verida/web3" "^2.2.1" + axios "^0.27.2" + ethers "^5.7.0" + "@verida/verifiable-credentials@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@verida/verifiable-credentials/-/verifiable-credentials-2.2.0.tgz#abd842f2968ca72678e17ec666d9b880905ad6ae" @@ -4167,6 +4198,14 @@ axios "^1.2.3" ethers "^5.7.0" +"@verida/web3@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@verida/web3/-/web3-2.2.1.tgz#16b338ed5b452a3269fdf2752a3d255ad7807744" + integrity sha512-5h6STyoKkZUDGVgoZt6CSOT09HzHdFV1MlshzDf4wqe2eT1hLzfyZsruSZiu20iiW/HgX4AwgBjH0o4Ga1ddrg== + dependencies: + axios "^1.2.3" + ethers "^5.7.0" + "@walletconnect/browser-utils@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.8.0.tgz#33c10e777aa6be86c713095b5206d63d32df0951"