UNPKG

@ylide/everscale

Version:

Ylide Protocol SDK implementation for EverScale blockchain

371 lines (337 loc) 12.2 kB
import { ProviderRpcClient } from 'everscale-inpage-provider'; import { IGenericAccount, AbstractWalletController, PublicKey, PublicKeyType, MessageKey, MessageChunks, WalletControllerFactory, sha256, unpackSymmetricalyEncryptedData, Uint256, hexToUint256, WalletEvent, YlideError, YlideErrorType, SwitchAccountCallback, } from '@ylide/sdk'; import SmartBuffer from '@ylide/smart-buffer'; import { MailerContract, RegistryContract } from '../contracts'; import { DEV_MAILER_ADDRESS, DEV_REGISTRY_ADDRESS, DEV_BROADCASTER_ADDRESS, MAILER_EVERSCALE_ADDRESS, REGISTRY_EVERSCALE_ADDRESS, BROADCASTER_EVERSCALE_ADDRESS, MAILER_VENOM_ADDRESS, REGISTRY_VENOM_ADDRESS, BROADCASTER_VENOM_ADDRESS, } from '../misc'; import { decodeContentMessageBody } from '../contracts/contractUtils'; export class EverscaleWalletController extends AbstractWalletController { ever: ProviderRpcClient; readonly mailerContract: MailerContract; readonly broadcasterContract: MailerContract; readonly registryContract: RegistryContract; private lastCurrentAccount: IGenericAccount | null = null; // on(event: WalletEvent.ACCOUNT_CHANGED, fn: (newAccount: IGenericAccount) => void, context?: any): this; // on(event: WalletEvent.BLOCKCHAIN_CHANGED, fn: (newBlockchain: string) => void, context?: any): this; // on(event: WalletEvent.LOGIN, fn: (newAccount: IGenericAccount) => void, context?: any): this; // on(event: WalletEvent.LOGOUT, fn: () => void, context?: any): this; constructor( private readonly options: { type?: 'everwallet' | 'venomwallet'; dev?: boolean; mailerContractAddress?: string; broadcasterContractAddress?: string; registryContractAddress?: string; endpoint?: string; onSwitchAccountRequest?: SwitchAccountCallback; } = {}, ) { super(options); if (typeof options.type === 'undefined') { throw new Error('You must provide network type for EverscaleWalletController'); } this.onSwitchAccountRequest = options?.onSwitchAccountRequest || null; this.ever = new ProviderRpcClient({ fallback: async () => (options.type === 'everwallet' ? window.__ever : (window as any).__venom), forceUseFallback: true, }); this.broadcasterContract = new MailerContract( this.ever, options.broadcasterContractAddress || (options.dev ? DEV_BROADCASTER_ADDRESS : options.type === 'venomwallet' ? BROADCASTER_VENOM_ADDRESS : BROADCASTER_EVERSCALE_ADDRESS), ); this.mailerContract = new MailerContract( this.ever, options.mailerContractAddress || (options.dev ? DEV_MAILER_ADDRESS : options.type === 'venomwallet' ? MAILER_VENOM_ADDRESS : MAILER_EVERSCALE_ADDRESS), ); this.registryContract = new RegistryContract( this.ever, options.registryContractAddress || (options.dev ? DEV_REGISTRY_ADDRESS : options.type === 'venomwallet' ? REGISTRY_VENOM_ADDRESS : REGISTRY_EVERSCALE_ADDRESS), ); } isMultipleAccountsSupported() { return false; } async init(): Promise<void> { await this.getAuthenticatedAccount(); const logoutSubscription = await this.ever.subscribe('loggedOut'); logoutSubscription.on('data', () => this.emit(WalletEvent.LOGOUT)); const networkSubscription = await this.ever.subscribe('networkChanged'); networkSubscription.on('data', data => { // tslint:disable-next-line console.log('networkSubscription data: ', data); }); const permissionsSubscription = await this.ever.subscribe('permissionsChanged'); permissionsSubscription.on('data', data => { const oldAccount = this.lastCurrentAccount; if (data.permissions.accountInteraction) { this.lastCurrentAccount = { blockchain: this.options.type === 'everwallet' ? 'everscale' : 'venom', address: data.permissions.accountInteraction.address.toString(), publicKey: PublicKey.fromHexString( PublicKeyType.EVERSCALE_NATIVE, data.permissions.accountInteraction.publicKey, ), }; if (oldAccount) { this.emit(WalletEvent.ACCOUNT_CHANGED, this.lastCurrentAccount); } else { this.emit(WalletEvent.LOGIN, this.lastCurrentAccount); } } else { if (oldAccount) { this.emit(WalletEvent.LOGOUT); } } }); } private async ensureAccount(needAccount: IGenericAccount) { let me = await this.getAuthenticatedAccount(); if (!me || me.address !== needAccount.address) { await this.switchAccountRequest(me, needAccount); me = await this.getAuthenticatedAccount(); } if (!me || me.address !== needAccount.address) { throw new YlideError(YlideErrorType.ACCOUNT_UNREACHABLE, { currentAccount: me, needAccount }); } } addressToUint256(address: string): Uint256 { return hexToUint256(address.split(':')[1].toLowerCase()); } async requestYlidePrivateKey(me: IGenericAccount): Promise<Uint8Array | null> { throw new Error('Method not available.'); } async signMagicString(account: IGenericAccount, magicString: string): Promise<Uint8Array> { await this.ensureAccount(account); const me = await this.getAuthenticatedAccount(); if (!me) { throw new Error(`Can't derive without auth`); } const result = await this.ever.signData({ publicKey: me.publicKey!.toHex(), data: SmartBuffer.ofUTF8String(magicString).toBase64String(), }); // @ts-ignore return sha256(SmartBuffer.ofHexString(result.signatureHex || result.signature_hex).bytes); } // account block async getAuthenticatedAccount(): Promise<IGenericAccount | null> { await this.ever.ensureInitialized(); const providerState = await this.ever.getProviderState(); if (providerState.permissions.accountInteraction) { this.lastCurrentAccount = { blockchain: this.options.type === 'everwallet' ? 'everscale' : 'venom', address: providerState.permissions.accountInteraction.address.toString(), publicKey: PublicKey.fromHexString( PublicKeyType.EVERSCALE_NATIVE, providerState.permissions.accountInteraction.publicKey, ), }; return this.lastCurrentAccount; } else { this.lastCurrentAccount = null; return null; } } async getCurrentBlockchain(): Promise<string> { return this.options.type === 'everwallet' ? 'everscale' : 'venom'; } async attachPublicKey(account: IGenericAccount, publicKey: Uint8Array) { await this.ensureAccount(account); // key version = 2 // registrar = 2 - everscale await this.registryContract.attachPublicKey(account.address, publicKey); } async requestAuthentication(): Promise<null | IGenericAccount> { const acc = await this.getAuthenticatedAccount(); if (acc) { await this.disconnectAccount(acc); } const { accountInteraction } = await this.ever.requestPermissions({ permissions: ['basic', 'accountInteraction'], }); if (accountInteraction) { return { blockchain: this.options.type === 'everwallet' ? 'everscale' : 'venom', address: accountInteraction.address.toString(), publicKey: PublicKey.fromHexString(PublicKeyType.EVERSCALE_NATIVE, accountInteraction.publicKey), }; } else { throw new Error('Not authenticated'); } } async disconnectAccount(account: IGenericAccount): Promise<void> { await this.ensureAccount(account); await this.ever.disconnect(); } async publishMessage( me: IGenericAccount, contentData: Uint8Array, recipients: { address: Uint256; messageKey: MessageKey }[], ): Promise<Uint256 | null> { await this.ensureAccount(me); const uniqueId = Math.floor(Math.random() * 4 * 10 ** 9); const chunks = MessageChunks.splitMessageChunks(contentData); if (chunks.length === 1 && recipients.length === 1) { const transaction = await this.mailerContract.sendSmallMail( me.address, uniqueId, recipients[0].address, recipients[0].messageKey.toBytes(), chunks[0], ); const om = transaction.childTransaction.outMessages; const contentMsg = om.length ? om[0] : null; if (!contentMsg || !contentMsg.body) { throw new Error('Content event was not found'); } const decodedEvent = decodeContentMessageBody(contentMsg.body!); return decodedEvent.msgId; } else if (chunks.length === 1 && recipients.length < Math.ceil((15.5 * 1024 - chunks[0].byteLength) / 70)) { const transaction = await this.mailerContract.sendBulkMail( me.address, uniqueId, recipients.map(r => r.address), recipients.map(r => r.messageKey.toBytes()), chunks[0], ); const om = transaction.childTransaction.outMessages; const contentMsg = om.length ? om[0] : null; if (!contentMsg || !contentMsg.body) { throw new Error('Content event was not found'); } const decodedEvent = decodeContentMessageBody(contentMsg.body!); return decodedEvent.msgId; } else { const initTime = Math.floor(Date.now() / 1000); const msgId = await this.mailerContract.buildHash(me.publicKey!.bytes, uniqueId, initTime); for (let i = 0; i < chunks.length; i++) { await this.mailerContract.sendMultipartMailPart( me.address, uniqueId, initTime, chunks.length, i, chunks[i], ); } for (let i = 0; i < recipients.length; i += 210) { const recs = recipients.slice(i, i + 210); await this.mailerContract.addRecipients( me.address, uniqueId, initTime, recs.map(r => r.address), recs.map(r => r.messageKey.toBytes()), ); } return msgId; } } async broadcastMessage(me: IGenericAccount, contentData: Uint8Array): Promise<Uint256 | null> { await this.ensureAccount(me); const uniqueId = Math.floor(Math.random() * 4 * 10 ** 9); const chunks = MessageChunks.splitMessageChunks(contentData); if (chunks.length === 1) { const transaction = await this.broadcasterContract.broadcastMail(me.address, uniqueId, chunks[0]); const om = transaction.childTransaction.outMessages; const contentMsg = om.length ? om[0] : null; if (!contentMsg || !contentMsg.body) { throw new Error('Content event was not found'); } const decodedEvent = decodeContentMessageBody(contentMsg.body!); return decodedEvent.msgId; } else { const initTime = Math.floor(Date.now() / 1000); const msgId = await this.broadcasterContract.buildHash(me.publicKey!.bytes, uniqueId, initTime); for (let i = 0; i < chunks.length; i++) { await this.broadcasterContract.sendMultipartMailPart( me.address, uniqueId, initTime, chunks.length, i, chunks[i], ); } await this.broadcasterContract.broadcastMailHeader(me.address, uniqueId, initTime); return msgId; } } async decryptMessageKey( recipientAccount: IGenericAccount, senderPublicKey: PublicKey, encryptedKey: Uint8Array, ): Promise<Uint8Array> { if (senderPublicKey.type !== PublicKeyType.EVERSCALE_NATIVE) { throw new Error('EverWallet can only decrypt native encryption of EverWallet'); } const { encData, nonce } = unpackSymmetricalyEncryptedData(encryptedKey); const decryptionResultBase64 = await this.ever.decryptData({ algorithm: 'ChaCha20Poly1305', sourcePublicKey: senderPublicKey.toHex(), recipientPublicKey: recipientAccount.publicKey!.toHex(), data: new SmartBuffer(encData).toBase64String(), nonce: new SmartBuffer(nonce).toBase64String(), }); return SmartBuffer.ofBase64String(decryptionResultBase64).bytes; } } export const everscaleWalletFactory: WalletControllerFactory = { create: async (options?: any) => new EverscaleWalletController(Object.assign({ type: 'everwallet' }, options || {})), isWalletAvailable: () => new ProviderRpcClient({ fallback: async () => window.__ever!, forceUseFallback: true }).hasProvider(), blockchainGroup: 'everscale', wallet: 'everwallet', }; export const venomWalletFactory: WalletControllerFactory = { create: async (options?: any) => new EverscaleWalletController(Object.assign({ type: 'venomwallet' }, options || {})), isWalletAvailable: () => new ProviderRpcClient({ fallback: async () => (window as any).__venom, forceUseFallback: true }).hasProvider(), blockchainGroup: 'venom', wallet: 'venomwallet', }; export function randomHex(length = 64) { return [...Array(length)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); }