diff --git a/src/keyring/handler.ts b/src/keyring/handler.ts index 0a2c8d2..a102ceb 100644 --- a/src/keyring/handler.ts +++ b/src/keyring/handler.ts @@ -37,7 +37,9 @@ import { RequestSignProxyReEncryptionDataMsg, RequestSignProxyDecryptionDataMsg, RequestSignBitcoinMsg, - TriggerSmartContractMsg + TriggerSmartContractMsg, + RequestSignOasisMsg, + GetDefaultAddressOasisMsg } from './messages'; import { KeyRingService } from './service'; import { Bech32Address } from '@owallet/cosmos'; @@ -87,6 +89,10 @@ export const getHandler: (service: KeyRingService) => Handler = (service: KeyRin return handleRequestSignBitcoinMsg(service)(env, msg as RequestSignBitcoinMsg); case RequestSignTronMsg: return handleRequestSignTronMsg(service)(env, msg as RequestSignTronMsg); + case RequestSignOasisMsg: + return handleRequestSignOasisMsg(service)(env, msg as RequestSignOasisMsg); + case GetDefaultAddressOasisMsg: + return handleGetDefaultAddressOasisMsg(service)(env, msg as GetDefaultAddressOasisMsg); case RequestSignEthereumTypedDataMsg: return handleRequestSignEthereumTypedData(service)(env, msg as RequestSignEthereumTypedDataMsg); case RequestPublicKeyMsg: @@ -125,68 +131,68 @@ export const getHandler: (service: KeyRingService) => Handler = (service: KeyRin }; }; -const handleRestoreKeyRingMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleRestoreKeyRingMsg: (service: KeyRingService) => InternalHandler = service => { return async (_env, _msg) => { return await service.restore(); }; }; -const handleDeleteKeyRingMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleDeleteKeyRingMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.deleteKeyRing(msg.index, msg.password); }; }; -const handleUpdateNameKeyRingMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleUpdateNameKeyRingMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.updateNameKeyRing(msg.index, msg.name, msg?.email); }; }; -const handleShowKeyRingMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleShowKeyRingMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.showKeyRing(msg.index, msg.password); }; }; -const handleCreateMnemonicKeyMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleCreateMnemonicKeyMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.createMnemonicKey(msg.kdf, msg.mnemonic, msg.password, msg.meta, msg.bip44HDPath); }; }; -const handleAddMnemonicKeyMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleAddMnemonicKeyMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.addMnemonicKey(msg.kdf, msg.mnemonic, msg.meta, msg.bip44HDPath); }; }; -const handleCreatePrivateKeyMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleCreatePrivateKeyMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.createPrivateKey(msg.kdf, msg.privateKey, msg.password, msg.meta); }; }; -const handleAddPrivateKeyMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleAddPrivateKeyMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.addPrivateKey(msg.kdf, msg.privateKey, msg.meta); }; }; -const handleCreateLedgerKeyMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleCreateLedgerKeyMsg: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { return await service.createLedgerKey(env, msg.kdf, msg.password, msg.meta, msg.bip44HDPath); }; }; -const handleAddLedgerKeyMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleAddLedgerKeyMsg: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { const result = await service.addLedgerKey(env, msg.kdf, msg.meta, msg.bip44HDPath); return result; }; }; -const handleLockKeyRingMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleLockKeyRingMsg: (service: KeyRingService) => InternalHandler = service => { return () => { return { status: service.lock() @@ -194,7 +200,7 @@ const handleLockKeyRingMsg: (service: KeyRingService) => InternalHandler InternalHandler = (service) => { +const handleUnlockKeyRingMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return { status: await service.unlock(msg.password, msg.saving) @@ -202,7 +208,7 @@ const handleUnlockKeyRingMsg: (service: KeyRingService) => InternalHandler InternalHandler = (service) => { +const handleGetKeyMsg: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { await service.permissionService.checkOrGrantBasicAccessPermission(env, msg.chainId, msg.origin); @@ -238,7 +244,7 @@ const handleGetKeyMsg: (service: KeyRingService) => InternalHandler = }; }; -const handleRequestSignAminoMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleRequestSignAminoMsg: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { await service.permissionService.checkOrGrantBasicAccessPermission(env, msg.chainId, msg.origin); @@ -248,7 +254,7 @@ const handleRequestSignAminoMsg: (service: KeyRingService) => InternalHandler InternalHandler = (service) => { +) => InternalHandler = service => { return async (env, msg) => { await service.permissionService.checkOrGrantBasicAccessPermission(env, msg.chainId, msg.origin); @@ -256,7 +262,7 @@ const handleRequestVerifyADR36AminoSignDoc: ( }; }; -const handleRequestSignDirectMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleRequestSignDirectMsg: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { await service.permissionService.checkOrGrantBasicAccessPermission(env, msg.chainId, msg.origin); @@ -290,14 +296,14 @@ const handleRequestSignDirectMsg: (service: KeyRingService) => InternalHandler InternalHandler = (service) => { +) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestSignEthereumTypedData(env, msg.chainId, msg.data?.[0]); return { result: JSON.stringify(response) }; }; }; -const handleRequestPublicKey: (service: KeyRingService) => InternalHandler = (service) => { +const handleRequestPublicKey: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestPublicKey(env, msg.chainId); return { result: JSON.stringify(response) }; @@ -306,7 +312,7 @@ const handleRequestPublicKey: (service: KeyRingService) => InternalHandler InternalHandler = (service) => { +) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestSignDecryptData(env, msg.chainId, msg.data); return { result: JSON.stringify(response) }; @@ -314,7 +320,7 @@ const handleRequestSignDecryptionData: ( }; const handleRequestSignEIP712CosmosTxMsg_v0: ( service: KeyRingService -) => InternalHandler = (service) => { +) => InternalHandler = service => { return async (env, msg) => { return await service.requestSignEIP712CosmosTx_v0_selected( env, @@ -329,16 +335,14 @@ const handleRequestSignEIP712CosmosTxMsg_v0: ( }; const handleRequestSignProxyDecryptionData: ( service: KeyRingService -) => InternalHandler = (service) => { +) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestSignProxyDecryptionData(env, msg.chainId, msg.data); return { result: JSON.stringify(response) }; }; }; -const handleGetDefaultAddressMsg: (service: KeyRingService) => InternalHandler = ( - service -) => { +const handleGetDefaultAddressMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { const key = await service.getKey(msg.chainId); const ledgerCheck = service.getKeyRingType(); @@ -364,7 +368,7 @@ const handleGetDefaultAddressMsg: (service: KeyRingService) => InternalHandler InternalHandler = (service) => { +) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestSignReEncryptData(env, msg.chainId, msg.data); @@ -372,16 +376,14 @@ const handleRequestSignProxyReEncryptionData: ( }; }; -const handleRequestSignBitcoinMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleRequestSignBitcoinMsg: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestSignBitcoin(env, msg.chainId, msg.data); return { rawTxHex: response }; }; }; -const handleRequestSignEthereumMsg: (service: KeyRingService) => InternalHandler = ( - service -) => { +const handleRequestSignEthereumMsg: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestSignEthereum(env, msg.chainId, msg.data); @@ -389,9 +391,9 @@ const handleRequestSignEthereumMsg: (service: KeyRingService) => InternalHandler }; }; -const handleGetMultiKeyStoreInfoMsg: (service: KeyRingService) => InternalHandler = ( - service -) => { +const handleGetMultiKeyStoreInfoMsg: ( + service: KeyRingService +) => InternalHandler = service => { return () => { return { multiKeyStoreInfo: service.getMultiKeyStoreInfo() @@ -399,77 +401,102 @@ const handleGetMultiKeyStoreInfoMsg: (service: KeyRingService) => InternalHandle }; }; -const handleChangeKeyRingMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleChangeKeyRingMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.changeKeyStoreFromMultiKeyStore(msg.index); }; }; -const handleChangeChainMsg: (service: any) => InternalHandler = (service) => { +const handleChangeChainMsg: (service: any) => InternalHandler = service => { return async (_, msg) => { return await service.changeChain(msg.chainInfos); }; }; -const handleGetIsKeyStoreCoinTypeSetMsg: (service: KeyRingService) => InternalHandler = ( - service -) => { +const handleGetIsKeyStoreCoinTypeSetMsg: ( + service: KeyRingService +) => InternalHandler = service => { return (_, msg) => { return service.getKeyStoreBIP44Selectables(msg.chainId, msg.paths); }; }; -const handleSetKeyStoreCoinTypeMsg: (service: KeyRingService) => InternalHandler = ( - service -) => { +const handleSetKeyStoreCoinTypeMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { await service.setKeyStoreCoinType(msg.chainId, msg.coinType); return service.keyRingStatus; }; }; -const handleSetKeyStoreLedgerAddressMsg: (service: KeyRingService) => InternalHandler = ( - service -) => { +const handleSetKeyStoreLedgerAddressMsg: ( + service: KeyRingService +) => InternalHandler = service => { return async (env, msg) => { await service.setKeyStoreLedgerAddress(env, msg.bip44HDPath, msg.chainId); return service.keyRingStatus; }; }; -const handleCheckPasswordMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleCheckPasswordMsg: (service: KeyRingService) => InternalHandler = service => { return (_, msg) => { return service.checkPassword(msg.password); }; }; -const handleExportKeyRingDatasMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleExportKeyRingDatasMsg: (service: KeyRingService) => InternalHandler = service => { return async (_, msg) => { return await service.exportKeyRingDatas(msg.password); }; }; -const handleRequestSignTronMsg: (service: KeyRingService) => InternalHandler = (service) => { +const handleRequestSignTronMsg: (service: KeyRingService) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestSignTron(env, msg.chainId, msg.data); return { ...response }; }; }; -const handleSendRawTransactionMsg: (service: KeyRingService) => InternalHandler = ( - service -) => { +const handleSendRawTransactionMsg: ( + service: KeyRingService +) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestSendRawTransaction(env, msg.chainId, msg.data); return { ...response }; }; }; -const handleTriggerSmartContractMsg: (service: KeyRingService) => InternalHandler = ( - service -) => { +const handleTriggerSmartContractMsg: ( + service: KeyRingService +) => InternalHandler = service => { return async (env, msg) => { const response = await service.requestTriggerSmartContract(env, msg.chainId, msg.data); return { ...response }; }; }; + +const handleRequestSignOasisMsg: (service: KeyRingService) => InternalHandler = service => { + return async (env, msg) => { + const response = await service.requestSignOasis(env, msg.chainId, msg.data); + return { ...response }; + }; +}; + +const handleGetDefaultAddressOasisMsg: ( + service: KeyRingService +) => InternalHandler = service => { + return async (_, msg) => { + const address = await service.getDefaultOasisAddress(msg.chainId); + const balance = await service.getDefaultOasisBalance(address, msg.chainId); + const ledgerCheck = service.getKeyRingType(); + + if (ledgerCheck === 'ledger') { + throw new Error('Ledger is currently not supported for Oasis.'); + } + return { + name: service.getKeyStoreMeta('name'), + type: Number(ledgerCheck), + address: address, + balance: balance + }; + }; +}; diff --git a/src/keyring/init.ts b/src/keyring/init.ts index 8689e78..6351bcd 100644 --- a/src/keyring/init.ts +++ b/src/keyring/init.ts @@ -31,8 +31,10 @@ import { RequestPublicKeyMsg, ChangeChainMsg, RequestSignTronMsg, + RequestSignOasisMsg, RequestSignBitcoinMsg, GetDefaultAddressTronMsg, + GetDefaultAddressOasisMsg, TriggerSmartContractMsg, RequestSendRawTransactionMsg } from './messages'; @@ -59,6 +61,7 @@ export function init(router: Router, service: KeyRingService): void { router.registerMessage(RequestSignDirectMsg); router.registerMessage(RequestSignEthereumMsg); router.registerMessage(RequestSignTronMsg); + router.registerMessage(RequestSignOasisMsg); router.registerMessage(TriggerSmartContractMsg); router.registerMessage(RequestSignBitcoinMsg); router.registerMessage(RequestSignEthereumTypedDataMsg); @@ -68,6 +71,7 @@ export function init(router: Router, service: KeyRingService): void { router.registerMessage(RequestSignReEncryptDataMsg); router.registerMessage(GetMultiKeyStoreInfoMsg); router.registerMessage(GetDefaultAddressTronMsg); + router.registerMessage(GetDefaultAddressOasisMsg); router.registerMessage(RequestSendRawTransactionMsg); router.registerMessage(ChangeKeyRingMsg); router.registerMessage(GetIsKeyStoreCoinTypeSetMsg); diff --git a/src/keyring/keyring.ts b/src/keyring/keyring.ts index 8c31e9b..c9c32fd 100644 --- a/src/keyring/keyring.ts +++ b/src/keyring/keyring.ts @@ -55,9 +55,20 @@ import { getAddressTypeByAddress } from '@owallet/bitcoin'; import { BIP44HDPath } from '@owallet/types'; -import { handleAddressLedgerByChainId } from '../utils/helper'; +import { getOasisNic, handleAddressLedgerByChainId } from '../utils/helper'; import { AddressesLedger } from '@owallet/types'; import { ChainsService } from '../chains'; +import * as oasis from '@oasisprotocol/client'; +import { + addressToPublicKey, + hex2uint, + parseRoseStringToBigNumber, + parseRpcBalance, + StringifiedBigInt, + uint2hex +} from '../utils/oasis-helper'; +import { OasisTransaction, signerFromPrivateKey } from '../utils/oasis-tx-builder'; + // inject TronWeb class (globalThis as any).TronWeb = require('tronweb'); export enum KeyRingStatus { @@ -569,7 +580,7 @@ export class KeyRing { [ChainIdHelper.parse(chainId).identifier]: coinType }; - const keyStoreInMulti = this.multiKeyStore.find((keyStore) => { + const keyStoreInMulti = this.multiKeyStore.find(keyStore => { return ( KeyRing.getKeyStoreId(keyStore) === // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -598,7 +609,7 @@ export class KeyRing { const { publicKey, address } = (await this.ledgerKeeper.getPublicKey(env, hdPath, ledgerAppType)) || {}; const pubKey = publicKey ? Buffer.from(publicKey).toString('hex') : null; - const keyStoreInMulti = this.multiKeyStore.find((keyStore) => { + const keyStoreInMulti = this.multiKeyStore.find(keyStore => { return ( KeyRing.getKeyStoreId(keyStore) === // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -775,6 +786,72 @@ export class KeyRing { legacyAddress: legacyAddress }; } + + public async loadPublicKeyOasis(): Promise { + if (this.status !== KeyRingStatus.UNLOCKED || this.type === 'none' || !this.keyStore) { + throw new Error('Key ring is not unlocked'); + } + if (!this.mnemonic) { + throw new Error('Key store type is mnemonic and it is unlocked. But, mnemonic is not loaded unexpectedly'); + } + const signer = await oasis.hdkey.HDKey.getAccountSigner(this.mnemonic, 0); + return signer.publicKey; + } + + public async signOasis(chainId: string, data): Promise { + if (this.status !== KeyRingStatus.UNLOCKED || this.type === 'none' || !this.keyStore) { + throw new Error('Key ring is not unlocked'); + } + if (!this.mnemonic) { + throw new Error('Key store type is mnemonic and it is unlocked. But, mnemonic is not loaded unexpectedly'); + } + + const { amount, to } = data; + + const chainInfo = await this.chainsService.getChainInfo(chainId as string); + + const nic = await getOasisNic(chainInfo.grpc); + const accountSigner = await oasis.hdkey.HDKey.getAccountSigner(this.mnemonic, 0); + const privateKey = uint2hex(accountSigner.secretKey); + const bytes = hex2uint(privateKey!); + const signer = signerFromPrivateKey(bytes); + const bigIntAmount = BigInt(parseRoseStringToBigNumber(amount).toString()); + console.log('bigIntAmount', bigIntAmount); + const chainContext = await nic.consensusGetChainContext(); + + const tw = await OasisTransaction.buildTransfer(nic, signer, to.replaceAll(' ', ''), bigIntAmount); + + await OasisTransaction.sign(chainContext, signer, tw); + + const payload = await OasisTransaction.submit(nic, tw); + + return payload; + } + + public async loadBalanceOasis( + address: string, + chainId: string + ): Promise<{ + available: StringifiedBigInt; + validator: { escrow: StringifiedBigInt; escrow_debonding: StringifiedBigInt }; + }> { + if (this.status !== KeyRingStatus.UNLOCKED || this.type === 'none' || !this.keyStore) { + throw new Error('Key ring is not unlocked'); + } + if (!this.mnemonic) { + throw new Error('Key store type is mnemonic and it is unlocked. But, mnemonic is not loaded unexpectedly'); + } + const chainInfo = await this.chainsService.getChainInfo(chainId as string); + + const nic = await getOasisNic(chainInfo.grpc); + + const publicKey = await addressToPublicKey(address); + const account = await nic.stakingAccount({ owner: publicKey, height: 0 }); + const grpcBalance = parseRpcBalance(account); + + return grpcBalance; + } + private loadPrivKey(coinType: number): PrivKeySecp256k1 { if (this.status !== KeyRingStatus.UNLOCKED || this.type === 'none' || !this.keyStore) { throw new Error('Key ring is not unlocked'); @@ -801,7 +878,6 @@ export class KeyRing { if (!this.mnemonic) { throw new Error('Key store type is mnemonic and it is unlocked. But, mnemonic is not loaded unexpectedly'); } - // could use it here const privKey = Mnemonic.generateWalletFromMnemonic(this.mnemonic, path); this.cached.set(path, privKey); return new PrivKeySecp256k1(privKey); @@ -1122,7 +1198,7 @@ export class KeyRing { const privKey = this.loadPrivKey(60); const privKeyBuffer = Buffer.from(privKey.toBytes()); const response = await Promise.all( - message[0].map(async (data) => { + message[0].map(async data => { const encryptedData = { ciphertext: Buffer.from(data.ciphertext, 'hex'), ephemPublicKey: Buffer.from(data.ephemPublicKey, 'hex'), @@ -1152,7 +1228,7 @@ export class KeyRing { } } - public async getPublicKey(chainId: string): Promise { + public async getPublicKey(chainId: string): Promise { if (this.status !== KeyRingStatus.UNLOCKED) { throw new Error('Key ring is not unlocked'); } @@ -1161,7 +1237,14 @@ export class KeyRing { throw new Error('Key Store is empty'); } + if (chainId === '0x5afe') { + const pubKey = await this.loadPublicKeyOasis(); + return pubKey; + } + const privKey = this.loadPrivKey(getCoinTypeByChainId(chainId)); + + // And Oasis here const pubKeyHex = '04' + privateToPublic(Buffer.from(privKey.toBytes())).toString('hex'); return pubKeyHex; @@ -1359,7 +1442,7 @@ export class KeyRing { throw new Error('Arrays are unimplemented in encodeData; use V4 extension'); } const parsedType = type.slice(0, type.lastIndexOf('[')); - const typeValuePairs = value.map((item) => this.encodeField(types, name, parsedType, item, version)); + const typeValuePairs = value.map(item => this.encodeField(types, name, parsedType, item, version)); return [ 'bytes32', keccak( diff --git a/src/keyring/messages.ts b/src/keyring/messages.ts index 030fbd7..cec94ca 100644 --- a/src/keyring/messages.ts +++ b/src/keyring/messages.ts @@ -1396,3 +1396,66 @@ export class ExportKeyRingDatasMsg extends Message { return ExportKeyRingDatasMsg.type(); } } + +// Oasis +// request sign Oasis goes here +export class RequestSignOasisMsg extends Message<{}> { + public static type() { + return 'request-sign-oasis'; + } + + constructor(public readonly chainId: string, public readonly data: object) { + super(); + } + + validateBasic(): void { + if (!this.chainId) { + throw new OWalletError('keyring', 270, 'chain id not set'); + } + + if (!this.data) { + throw new OWalletError('keyring', 231, 'data not set'); + } + } + + approveExternal(): boolean { + return true; + } + + route(): string { + return ROUTE; + } + + type(): string { + return RequestSignOasisMsg.type(); + } +} + +export class GetDefaultAddressOasisMsg extends Message<{ + hex?: string; + address?: string; + name?: string; + type?: number; +}> { + public static type() { + return 'get-default-address-oasis'; + } + + constructor(public readonly chainId: string) { + super(); + } + + validateBasic(): void { + if (!this.chainId) { + throw new OWalletError('keyring', 270, 'chain id not set'); + } + } + + route(): string { + return ROUTE; + } + + type(): string { + return GetDefaultAddressOasisMsg.type(); + } +} diff --git a/src/keyring/service.ts b/src/keyring/service.ts index 2d68024..7482da6 100644 --- a/src/keyring/service.ts +++ b/src/keyring/service.ts @@ -38,6 +38,8 @@ import { request } from '../tx'; import { Dec, DecUtils } from '@owallet/unit'; import { trimAminoSignDoc } from './amino-sign-doc'; import { KeyringHelper } from './utils'; +import * as oasis from '@oasisprotocol/client'; + @singleton() export class KeyRingService { private readonly keyRing: KeyRing; @@ -513,8 +515,7 @@ export class KeyRingService { async requestPublicKey(env: Env, chainId: string): Promise { try { - const rawTxHex = await this.keyRing.getPublicKey(chainId); - + const rawTxHex = (await this.keyRing.getPublicKey(chainId)) as string; return rawTxHex; } catch (e) { console.log('e', e.message); @@ -848,4 +849,25 @@ export class KeyRingService { this.interactionService.dispatchEvent(APP_PORT, 'request-sign-tron-end', {}); } } + + async requestSignOasis(env: Env, chainId: string, data: object): Promise { + try { + const tx = await this.keyRing.signOasis(chainId, data); + return tx; + } finally { + this.interactionService.dispatchEvent(APP_PORT, 'request-sign-oasis-end', {}); + } + } + + async getDefaultOasisAddress(chainId: string): Promise { + const signerPublicKey = await this.keyRing.loadPublicKeyOasis(); + const addressUint8Array = await oasis.staking.addressFromPublicKey(signerPublicKey); + const address = oasis.staking.addressToBech32(addressUint8Array); + return address; + } + + async getDefaultOasisBalance(address: string, chainId: string): Promise { + const balance = await this.keyRing.loadBalanceOasis(address, chainId); + return balance.available; + } } diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 41d30a4..996f42f 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -4,6 +4,8 @@ import { LedgerAppType, getNetworkTypeByChainId, typeBtcLedgerByAddress } from ' import { AddressBtcType, AddressesLedger, BIP44HDPath, ChainInfoWithoutEndpoints } from '@owallet/types'; import Joi from 'joi'; import { ChainInfoWithEmbed } from 'src/chains'; +import * as oasis from '@oasisprotocol/client'; + export const EIP712DomainTypeValidator = Joi.array() .items( Joi.object<{ @@ -48,7 +50,7 @@ export const EIP712DomainTypeValidator = Joi.array() ) .unique() .min(1) - .custom((value) => { + .custom(value => { // Sort by name const domainFieldNames: Array = ['name', 'version', 'chainId', 'verifyingContract', 'salt']; @@ -158,3 +160,12 @@ export const handleAddressLedgerByChainId = ( [typeBtcLedgerByAddress(chainInfo, addressType)]: address }; }; + +/** + * Return a nic client for the specified network, + * or by default, for the currently selected network + */ +export const getOasisNic = async url => { + const nic = await new oasis.client.NodeInternal(url); + return nic; +}; diff --git a/src/utils/oasis-helper.ts b/src/utils/oasis-helper.ts new file mode 100644 index 0000000..6331f15 --- /dev/null +++ b/src/utils/oasis-helper.ts @@ -0,0 +1,127 @@ +import { quantity, staking, types } from '@oasisprotocol/client'; +import BigNumber from 'bignumber.js'; +/** Redux can't serialize bigint fields, so we stringify them, and mark them. */ +export type StringifiedBigInt = string & PreserveAliasName; +// eslint-disable-next-line @typescript-eslint/ban-types +interface PreserveAliasName extends String {} +type ParaTimeNetwork = { + address: string | undefined; + runtimeId: string | undefined; +}; +export enum RuntimeTypes { + Evm = 'evm', + Oasis = 'oasis' +} +export type ParaTimeConfig = { + mainnet: ParaTimeNetwork; + testnet: ParaTimeNetwork; + local: ParaTimeNetwork; + gasPrice: bigint; + feeGas: bigint; + decimals: number; + displayOrder: number; + type: RuntimeTypes; +}; + +// Hover to check if inferred variable type is StringifiedBigInt (not string) +export const testPreserveAliasName = '0' as StringifiedBigInt; + +export const uint2hex = (uint: Uint8Array) => Buffer.from(uint).toString('hex'); +export const hex2uint = (hex: string) => new Uint8Array(Buffer.from(hex, 'hex')); + +export const shortPublicKey = async (publicKey: Uint8Array) => { + return await staking.addressFromPublicKey(publicKey); +}; + +export const publicKeyToAddress = async (publicKey: Uint8Array) => { + const data = await staking.addressFromPublicKey(publicKey); + return staking.addressToBech32(data); +}; + +export const addressToPublicKey = async (addr: string) => { + return staking.addressFromBech32(addr); +}; + +export const uint2bigintString = (uint: Uint8Array): StringifiedBigInt => quantity.toBigInt(uint).toString(); +export const stringBigint2uint = (number: StringifiedBigInt) => quantity.fromBigInt(BigInt(number)); + +export function concat(...parts: Uint8Array[]) { + let length = 0; + for (const part of parts) { + length += part.length; + } + const result = new Uint8Array(length); + let pos = 0; + for (const part of parts) { + result.set(part, pos); + pos += part.length; + } + return result; +} + +export function parseRoseStringToBigNumber(value: string, decimals = 9): BigNumber { + const baseUnitBN = new BigNumber(value).shiftedBy(decimals); // * 10 ** decimals + if (baseUnitBN.isNaN()) { + throw new Error(`not a number in parseRoseStringToBigNumber(${value})`); + } + if (baseUnitBN.decimalPlaces()! > 0) { + console.error('lost precision in parseRoseStringToBigNumber(', value); + } + return baseUnitBN.decimalPlaces(0); +} + +export function parseRoseStringToBaseUnitString(value: string): StringifiedBigInt { + const baseUnitBN = parseRoseStringToBigNumber(value); + return BigInt(baseUnitBN.toFixed(0)).toString(); +} + +function getRoseString(roseBN: BigNumber, minimumFractionDigits: number, maximumFractionDigits: number) { + return roseBN.toFormat(Math.min(Math.max(roseBN.decimalPlaces()!, minimumFractionDigits), maximumFractionDigits)); +} + +export function isAmountGreaterThan(amount: string, value: string) { + return parseRoseStringToBigNumber(amount).isGreaterThan(parseRoseStringToBigNumber(value)); +} + +export function formatBaseUnitsAsRose( + amount: StringifiedBigInt, + { minimumFractionDigits = 0, maximumFractionDigits = Infinity } = {} +) { + const roseBN = new BigNumber(amount).shiftedBy(-9); // / 10 ** 9 + return getRoseString(roseBN, minimumFractionDigits, maximumFractionDigits); +} + +export function formatWeiAsWrose( + amount: StringifiedBigInt, + { minimumFractionDigits = 0, maximumFractionDigits = Infinity } = {} +) { + const roseBN = new BigNumber(amount).shiftedBy(-18); // / 10 ** 18 + return getRoseString(roseBN, minimumFractionDigits, maximumFractionDigits); +} + +export function parseRpcBalance(account: types.StakingAccount) { + const zero = stringBigint2uint('0'); + + return { + available: uint2bigintString(account.general?.balance || zero), + validator: { + escrow: uint2bigintString(account.escrow?.active?.balance || zero), + escrow_debonding: uint2bigintString(account.escrow?.debonding?.balance || zero) + } + }; +} + +export function formatCommissionPercent(commission: number): string { + return new BigNumber(commission).times(100).toFormat(); +} + +export function getFeeAmount(gasPrice: bigint, feeGas: bigint): string { + // A wild guess: the minimum gas price times the default loose + // overestimate of the gas. + return (gasPrice * feeGas).toString(); +} + +const defaultDepositFeeAmount = '0'; +export const getDefaultFeeAmount = (isDepositing: boolean, paraTimeConfig: ParaTimeConfig): string => { + return isDepositing ? defaultDepositFeeAmount : getFeeAmount(paraTimeConfig.feeGas, paraTimeConfig.gasPrice); +}; diff --git a/src/utils/oasis-tx-builder.ts b/src/utils/oasis-tx-builder.ts new file mode 100644 index 0000000..6e86746 --- /dev/null +++ b/src/utils/oasis-tx-builder.ts @@ -0,0 +1,188 @@ +import * as oasis from '@oasisprotocol/client'; +import * as oasisRT from '@oasisprotocol/client-rt'; +import { ContextSigner, Signer } from '@oasisprotocol/client/dist/signature'; +import { addressToPublicKey, shortPublicKey } from './oasis-helper'; + +type OasisClient = oasis.client.NodeInternal; +export class WalletError extends Error { + constructor(public readonly type: WalletErrors, message: string, public readonly originalError?: Error) { + super(message); + } +} + +export enum WalletErrors { + UnknownError = 'unknown', + UnknownGrpcError = 'unknown_grpc', + InvalidAddress = 'invalid_address', + InvalidPrivateKey = 'invalid_private_key', + InsufficientBalance = 'insufficient_balance', + CannotSendToSelf = 'cannot_send_to_self', + InvalidNonce = 'invalid_nonce', + DuplicateTransaction = 'duplicate_transaction', + NoOpenWallet = 'no_open_wallet', + USBTransportError = 'usb_transport_error', + USBTransportNotSupported = 'usb_transport_not_supported', + BluetoothTransportNotSupported = 'bluetooth_transport_not_supported', + LedgerUnknownError = 'unknown_ledger_error', + LedgerCannotOpenOasisApp = 'cannot_open_oasis_app', + LedgerOasisAppIsNotOpen = 'oasis_app_is_not_open', + LedgerNoDeviceSelected = 'no_device_selected', + LedgerTransactionRejected = 'transaction_rejected', + LedgerAppVersionNotSupported = 'ledger_version_not_supported', + LedgerDerivedDifferentAccount = 'ledger_derived_different_account', + IndexerAPIError = 'indexer_api_error', + DisconnectedError = 'disconnected_error', + ParaTimesUnknownError = 'para_times_unknown_error' +} + +export const signerFromPrivateKey = (privateKey: Uint8Array) => { + return oasis.signature.NaclSigner.fromSecret(privateKey, 'this key is not important'); +}; + +export const signerFromEthPrivateKey = (ethPrivateKey: Uint8Array) => { + return oasisRT.signatureSecp256k1.EllipticSigner.fromPrivate(ethPrivateKey, 'this key is not important'); +}; + +/** Transaction Wrapper */ +export type TW = oasis.consensus.TransactionWrapper; + +/** Runtime Transaction Wrapper */ +type RTW = oasisRT.wrapper.TransactionWrapper; + +export class OasisTransaction { + protected static genesis?: oasis.types.GenesisDocument; + + public static async buildReclaimEscrow( + nic: OasisClient, + signer: Signer, + account: string, + shares: bigint + ): Promise> { + const tw = oasis.staking.reclaimEscrowWrapper(); + const nonce = await OasisTransaction.getNonce(nic, signer); + tw.setNonce(nonce); + tw.setFeeAmount(oasis.quantity.fromBigInt(0n)); + tw.setBody({ + account: await addressToPublicKey(account), + shares: oasis.quantity.fromBigInt(shares) + }); + + const gas = await tw.estimateGas(nic, signer.public()); + tw.setFeeGas(gas); + + return tw; + } + + public static async buildAddEscrow( + nic: OasisClient, + signer: Signer, + account: string, + amount: bigint + ): Promise> { + const tw = oasis.staking.addEscrowWrapper(); + const nonce = await OasisTransaction.getNonce(nic, signer); + tw.setNonce(nonce); + tw.setFeeAmount(oasis.quantity.fromBigInt(0n)); + tw.setBody({ + account: await addressToPublicKey(account), + amount: oasis.quantity.fromBigInt(amount) + }); + + const gas = await tw.estimateGas(nic, signer.public()); + tw.setFeeGas(gas); + + return tw; + } + + public static async buildTransfer( + nic: OasisClient, + signer: Signer, + to: string, + amount: bigint + ): Promise> { + const tw = oasis.staking.transferWrapper(); + const nonce = await OasisTransaction.getNonce(nic, signer); + tw.setNonce(nonce); + tw.setFeeAmount(oasis.quantity.fromBigInt(0n)); + tw.setBody({ + to: await addressToPublicKey(to), + amount: oasis.quantity.fromBigInt(amount) + }); + + console.log('tw', tw); + + const gas = await tw.estimateGas(nic, signer.public()); + tw.setFeeGas(gas); + + return tw; + } + + public static async buildStakingAllowTransfer( + nic: OasisClient, + signer: Signer, + to: string, + amount: bigint + ): Promise> { + const tw = oasis.staking.allowWrapper(); + const nonce = await OasisTransaction.getNonce(nic, signer); + const beneficiary = await addressToPublicKey(to); + + tw.setNonce(nonce); + tw.setFeeAmount(oasis.quantity.fromBigInt(0n)); + tw.setBody({ + beneficiary, + negative: false, + amount_change: oasis.quantity.fromBigInt(amount) + }); + + const gas = await tw.estimateGas(nic, signer.public()); + tw.setFeeGas(gas); + + return tw; + } + + public static async signUsingLedger(chainContext: string, signer: ContextSigner, tw: TW): Promise { + await tw.sign(signer, chainContext); + + // @todo Upstream bug in oasis-app, the signature is larger than 64 bytes + tw.signedTransaction.signature.signature = tw.signedTransaction.signature.signature.slice(0, 64); + } + + public static async sign(chainContext: string, signer: Signer, tw: TW): Promise { + return tw.sign(new oasis.signature.BlindContextSigner(signer), chainContext); + } + + public static async signParaTime(chainContext: string, signer: Signer, tw: RTW): Promise { + return tw.sign([new oasis.signature.BlindContextSigner(signer)], chainContext); + } + + public static async submit(nic: OasisClient, tw: TW | RTW): Promise { + try { + await tw.submit(nic); + } catch (e: any) { + const grpcError = e?.cause?.metadata?.['grpc-message'] || e.message; + + if (!grpcError) { + throw new WalletError(WalletErrors.UnknownError, grpcError, e); + } + + switch (grpcError) { + case 'transaction: invalid nonce': + throw new WalletError(WalletErrors.InvalidNonce, 'Invalid nonce'); + case 'consensus: duplicate transaction': + throw new WalletError(WalletErrors.DuplicateTransaction, 'Duplicate transaction'); + default: + throw new WalletError(WalletErrors.UnknownGrpcError, grpcError, e); + } + } + } + + protected static async getNonce(nic: OasisClient, signer: Signer): Promise { + const nonce = await nic.consensusGetSignerNonce({ + account_address: await shortPublicKey(signer.public()), + height: 0 + }); + + return BigInt(nonce || 0); + } +} diff --git a/tsconfig.json b/tsconfig.json index ead13f0..cebc518 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "outDir": "build", "declaration": true, "rootDir": "src", - "emitDecoratorMetadata": true + "emitDecoratorMetadata": true, + "target": "es2020", }, "include": ["src/**/*"] } \ No newline at end of file