diff --git a/packages/yoroi-ergo-connector/example-cardano/index.html b/packages/yoroi-ergo-connector/example-cardano/index.html index e292f87191..17afaef849 100644 --- a/packages/yoroi-ergo-connector/example-cardano/index.html +++ b/packages/yoroi-ergo-connector/example-cardano/index.html @@ -57,6 +57,10 @@

Cardano dApp Example

+
+ + +
Go to a subpage
diff --git a/packages/yoroi-ergo-connector/example-cardano/index.js b/packages/yoroi-ergo-connector/example-cardano/index.js index 79b602e494..2f2544de77 100644 --- a/packages/yoroi-ergo-connector/example-cardano/index.js +++ b/packages/yoroi-ergo-connector/example-cardano/index.js @@ -22,6 +22,7 @@ const submitTx = document.querySelector('#submit-tx') const signTx = document.querySelector('#sign-tx') const createTx = document.querySelector('#create-tx') const getCollateralUtxos = document.querySelector('#get-collateral-utxos') +const signData = document.querySelector('#sign-data') const alertEl = document.querySelector('#alert') const spinner = document.querySelector('#spinner') @@ -30,6 +31,7 @@ let cardanoApi let returnType = 'cbor' let utxos let usedAddresses +let unusedAddresses let changeAddress let unsignedTransactionHex let transactionHex @@ -211,6 +213,7 @@ getUnUsedAddresses.addEventListener('click', () => { return; } addresses = addressesFromCborIfNeeded(addresses) + unusedAddresses = addresses alertSuccess(`Address: `) alertEl.innerHTML = '

Unused addresses:

' + JSON.stringify(addresses, undefined, 2) + '
' }); @@ -626,11 +629,12 @@ createTx.addEventListener('click', () => { getCollateralUtxos.addEventListener('click', () => { toggleSpinner('show'); - + if (!accessGranted) { alertError('Should request access first'); return; } + const amount = '4900000'; cardanoApi.getCollateralUtxos( Buffer.from( @@ -649,6 +653,41 @@ getCollateralUtxos.addEventListener('click', () => { }) }) +signData.addEventListener('click', () => { + toggleSpinner('show'); + + if (!accessGranted) { + alertError('Should request access first'); + return; + } + + let address; + if (usedAddresses && usedAddresses.length > 0) { + address = usedAddresses[0]; + } else if (unusedAddresses && unusedAddresses.length > 0) { + address = unusedAddresses[0]; + } else { + alertError('Should request used or unused addresses first'); + return; + } + + const payload = document.querySelector('#sign-data-payload').value; + let payloadHex; + if (payload.startsWith('0x')) { + payloadHex = Buffer.from(payload.replace('^0x', ''), 'hex').toString('hex'); + } else { + payloadHex = Buffer.from(payload, 'utf8').toString('hex'); + } + cardanoApi.signData(address, payloadHex).then(sig => { + alertSuccess('Signature:' + JSON.stringify(sig)) + }).catch(error => { + console.error(error); + alertError(error.info); + }).then(() => { + toggleSpinner('hide'); + }); +}); + function alertError (text) { toggleSpinner('hide'); alertEl.className = 'alert alert-danger' diff --git a/packages/yoroi-ergo-connector/src/inject.js b/packages/yoroi-ergo-connector/src/inject.js index 3921e083a8..84302a7f31 100644 --- a/packages/yoroi-ergo-connector/src/inject.js +++ b/packages/yoroi-ergo-connector/src/inject.js @@ -254,9 +254,8 @@ class CardanoAPI { return this._cardano_rpc_call('sign_tx/cardano', [{ tx, partialSign, returnTx }]); } - signData(address, sigStructure) { - // TODO - throw new Error('Not implemented yet'); + signData(address, payload) { + return this._cardano_rpc_call("sign_data", [address, payload]); } getCollateralUtxos(requiredAmount) { diff --git a/packages/yoroi-extension/app/ergo-connector/api/index.js b/packages/yoroi-extension/app/ergo-connector/api/index.js index 64bab13b6d..4b3a562cd8 100644 --- a/packages/yoroi-extension/app/ergo-connector/api/index.js +++ b/packages/yoroi-extension/app/ergo-connector/api/index.js @@ -43,11 +43,11 @@ export const createAuthEntry: ({| stakingKey.to_public().hash() ) ).to_address(); - const entropy = await cip8Sign( + const entropy = (await cip8Sign( Buffer.from(address.to_bytes()), derivedSignKey, Buffer.from(`DAPP_LOGIN: ${appAuthID}`, 'utf8'), - ); + )).signature(); const appPrivKey = RustModule.WalletV4.Bip32PrivateKey.from_bip39_entropy( entropy, @@ -72,12 +72,11 @@ export const authSignHexPayload: ({| return appPrivKey.sign(Buffer.from(payloadHex, 'hex')).to_hex(); } -// return the hex string representation of the COSESign1 -const cip8Sign = async ( +export const cip8Sign = async ( address: Buffer, signKey: RustModule.WalletV4.PrivateKey, payload: Buffer, -): Promise => { +): Promise => { const protectedHeader = RustModule.MessageSigning.HeaderMap.new(); protectedHeader.set_algorithm_id( RustModule.MessageSigning.Label.from_algorithm_id( @@ -94,6 +93,5 @@ const cip8Sign = async ( const builder = RustModule.MessageSigning.COSESign1Builder.new(headers, payload, false); const toSign = builder.make_data_to_sign().to_bytes(); const signedSigStruct = signKey.sign(toSign).to_bytes(); - const coseSign1 = builder.build(signedSigStruct); - return Buffer.from(coseSign1.signature()); + return builder.build(signedSigStruct); } diff --git a/packages/yoroi-extension/app/ergo-connector/components/signin/CardanoSignTxPage.js b/packages/yoroi-extension/app/ergo-connector/components/signin/CardanoSignTxPage.js index 7c0960c335..954735d8b2 100644 --- a/packages/yoroi-extension/app/ergo-connector/components/signin/CardanoSignTxPage.js +++ b/packages/yoroi-extension/app/ergo-connector/components/signin/CardanoSignTxPage.js @@ -49,7 +49,7 @@ import { LoadingButton } from '@mui/lab'; import NoDappIcon from '../../../assets/images/dapp-connector/no-dapp.inline.svg'; type Props = {| - +txData: CardanoConnectorSignRequest, + +txData: ?CardanoConnectorSignRequest, +onCopyAddressTooltip: (string, string) => void, +onCancel: () => void, +onConfirm: string => Promise, @@ -66,6 +66,7 @@ type Props = {| +connectedWebsite: ?WhitelistEntry, +isReorg: boolean, +submissionError: ?SignSubmissionErrorType, + +signData: ?{| address: string, payload: string |}, |}; const messages = defineMessages({ @@ -298,6 +299,14 @@ class SignTxPage extends Component { ); }; + renderPayload(payloadHex: string): string { + const utf8 = Buffer.from(payloadHex, 'hex').toString('utf8'); + if (utf8.match(/^[\P{C}\t\r\n]+$/u)) { + return utf8; + } + return payloadHex; + } + render(): Node { const { form } = this; const walletPasswordField = form.$('walletPassword'); @@ -309,6 +318,7 @@ class SignTxPage extends Component { connectedWebsite, isReorg, submissionError, + signData, } = this.props; const { isSubmitting } = this.state; @@ -316,10 +326,111 @@ class SignTxPage extends Component { const url = connectedWebsite?.url ?? ''; const faviconUrl = connectedWebsite?.image ?? ''; - const txAmountDefaultToken = txData.amount.defaults.defaultIdentifier; - const txAmount = txData.amount.get(txAmountDefaultToken) ?? new BigNumber('0'); - const txFeeAmount = new BigNumber(txData.fee.amount).negated(); - const txTotalAmount = txAmount.plus(txFeeAmount); + let content; + let utxosContent; + if (txData) { + // signing a tx + const txAmountDefaultToken = txData.amount.defaults.defaultIdentifier; + const txAmount = txData.amount.get(txAmountDefaultToken) ?? new BigNumber('0'); + const txFeeAmount = new BigNumber(txData.fee.amount).negated(); + const txTotalAmount = txAmount.plus(txFeeAmount); + content = ( + + + {intl.formatMessage(signTxMessages.totals)} + + + + {intl.formatMessage(signTxMessages.transactionFee)} + + {this.renderAmountDisplay({ + entry: { + identifier: txData.fee.tokenId, + networkId: txData.fee.networkId, + amount: txFeeAmount, + }, + })} + + + + {intl.formatMessage(signTxMessages.totalAmount)} + + {this.renderAmountDisplay({ + entry: { + identifier: txAmountDefaultToken, + networkId: txData.amount.defaults.defaultNetworkId, + amount: txTotalAmount, + }, + })} + + + + + ); + utxosContent = ( + + + + ); + } else if (signData) { + // signing data + content = ( + + + {intl.formatMessage(signTxMessages.signMessage)} + + +
+              {this.renderPayload(signData.payload)}
+            
+
+
+ ); + utxosContent = null; + } else { + return null; + } + return ( { getTokenInfo={this.props.getTokenInfo} /> - - - {intl.formatMessage(signTxMessages.totals)} - - - - {intl.formatMessage(signTxMessages.transactionFee)} - - {this.renderAmountDisplay({ - entry: { - identifier: txData.fee.tokenId, - networkId: txData.fee.networkId, - amount: txFeeAmount, - }, - })} - - - - {intl.formatMessage(signTxMessages.totalAmount)} - - {this.renderAmountDisplay({ - entry: { - identifier: txAmountDefaultToken, - networkId: txData.amount.defaults.defaultNetworkId, - amount: txTotalAmount, - }, - })} - - - - - + {content} { } - utxoAddressContent={ - - - - } + utxoAddressContent={utxosContent} /> ); } diff --git a/packages/yoroi-extension/app/ergo-connector/components/signin/SignTxPage.js b/packages/yoroi-extension/app/ergo-connector/components/signin/SignTxPage.js index fae6d6fdc7..5b4df6c9b2 100644 --- a/packages/yoroi-extension/app/ergo-connector/components/signin/SignTxPage.js +++ b/packages/yoroi-extension/app/ergo-connector/components/signin/SignTxPage.js @@ -93,6 +93,10 @@ export const signTxMessages: Object = defineMessages({ id: 'api.errors.IncorrectPasswordError', defaultMessage: '!!!Incorrect wallet password.', }, + signMessage: { + id: 'connector.signin.signMessage', + defaultMessage: '!!!Sign Message', + }, }); type State = {| diff --git a/packages/yoroi-extension/app/ergo-connector/containers/SignTxContainer.js b/packages/yoroi-extension/app/ergo-connector/containers/SignTxContainer.js index 31609ef7d0..47b98b605f 100644 --- a/packages/yoroi-extension/app/ergo-connector/containers/SignTxContainer.js +++ b/packages/yoroi-extension/app/ergo-connector/containers/SignTxContainer.js @@ -153,10 +153,14 @@ export default class SignTxContainer extends Component< ); break; } - case 'tx/cardano': - case 'tx-reorg/cardano': { + case 'tx-reorg/cardano': + case 'data': + case 'tx/cardano': { const txData = this.generated.stores.connector.adaTransaction; - if (txData == null) return this.renderLoading(); + const signData = signingMessage.sign.type === 'data' + ? { address: signingMessage.sign.address, payload: signingMessage.sign.payload } + : null; + if (txData == null && signData == null) return this.renderLoading(); component = ( ); break; diff --git a/packages/yoroi-extension/app/ergo-connector/stores/ConnectorStore.js b/packages/yoroi-extension/app/ergo-connector/stores/ConnectorStore.js index ed47db161d..f7d2414068 100644 --- a/packages/yoroi-extension/app/ergo-connector/stores/ConnectorStore.js +++ b/packages/yoroi-extension/app/ergo-connector/stores/ConnectorStore.js @@ -327,9 +327,6 @@ export default class ConnectorStore extends Store { throw new Error(`${nameof(this._confirmSignInTx)} confirming a tx but no signing message set`); } const { signingMessage } = this; - if (signingMessage.sign.tx == null) { - throw new Error(`${nameof(this._confirmSignInTx)} signing non-tx is not supported`); - } const wallet = this.wallets.find(w => w.publicDeriver.getPublicDeriverId() === this.signingMessage?.publicDeriverId ); @@ -401,6 +398,14 @@ export default class ConnectorStore extends Store { tabId: signingMessage.tabId, pw: password, }; + } else if (signingMessage.sign.type === 'data') { + sendData = { + type: 'sign_confirmed', + tx: null, + uid: signingMessage.sign.uid, + tabId: signingMessage.tabId, + pw: password, + }; } else { throw new Error(`unkown sign data type ${signingMessage.sign.type}`); } diff --git a/packages/yoroi-extension/app/i18n/locales/en-US.json b/packages/yoroi-extension/app/i18n/locales/en-US.json index d7cbe2d2f6..0aa90073d5 100644 --- a/packages/yoroi-extension/app/i18n/locales/en-US.json +++ b/packages/yoroi-extension/app/i18n/locales/en-US.json @@ -72,6 +72,7 @@ "connector.signin.connectedTo": "Connected To", "connector.signin.more": "more", "connector.signin.receiver": "Receiver", + "connector.signin.signMessage": "Sign Message", "connector.signin.title": "Sign transaction", "connector.signin.totalAmount": "Total Amount", "connector.signin.totals": "Totals", diff --git a/packages/yoroi-extension/chrome/extension/background.js b/packages/yoroi-extension/chrome/extension/background.js index 4d46cb89c5..31a183bbad 100644 --- a/packages/yoroi-extension/chrome/extension/background.js +++ b/packages/yoroi-extension/chrome/extension/background.js @@ -33,6 +33,7 @@ import { asTx, asValue, ConnectorError, + DataSignErrorCodes, } from './ergo-connector/types'; import { connectorCreateCardanoTx, @@ -51,6 +52,8 @@ import { connectorSendTxCardano, connectorSignCardanoTx, connectorSignTx, + getAddressing, + connectorSignData, } from './ergo-connector/api'; import { updateTransactions as ergoUpdateTransactions } from '../../app/api/ergo/lib/storage/bridge/updateTransactions'; import { @@ -525,8 +528,9 @@ const yoroiMessageHandler = async ( } break; case 'data': - // mocked data sign - responseData.resolve({ err: 'Generic data signing is not implemented yet' }); + { + responseData.resolve({ ok: { password } }); + } break; case 'tx-reorg/cardano': { @@ -1042,18 +1046,86 @@ function handleInjectorConnect(port) { handleError(e); } break; - // unsupported until EIP-0012's definition is finalized - // case 'sign_data': - // { - // const resp = await confirmSign(tabId, { - // type: 'data', - // address: message.params[0], - // bytes: message.params[1], - // uid: message.uid - // }); - // rpcResponse(resp); - // } - // break; + case 'sign_data': + try { + const rawAddress = message.params[0]; + const payload = message.params[1]; + await withDb(async (db, localStorageApi) => { + await withSelectedWallet( + tabId, + async (wallet) => { + if (isCardano) { + await RustModule.load(); + const connection = connectedSites.get(tabId); + if (connection == null) { + Logger.error(`ERR - sign_data could not find connection with tabId = ${tabId}`); + rpcResponse(undefined); // shouldn't happen + return; + } + let address; + try { + address = Buffer.from( + RustModule.WalletV4.Address.from_bech32(rawAddress).to_bytes() + ).toString('hex'); + } catch { + address = rawAddress; + } + const addressing = await getAddressing(wallet, address); + if (!addressing) { + rpcResponse({ + err: { + code: DataSignErrorCodes.DATA_SIGN_ADDRESS_NOT_PK, + info: 'address not found', + } + }); + return; + } + const resp = await confirmSign( + tabId, + { + type: 'data', + address, + payload, + uid: message.uid + }, + connection, + ); + if (!resp.ok) { + rpcResponse(resp); + return; + } + let dataSig; + try { + dataSig = await connectorSignData( + wallet, + resp.ok.password, + addressing, + address, + payload, + ); + } catch (error) { + Logger.error(`error when signing data ${error}`); + rpcResponse({ + err: { + code: DataSignErrorCodes.DATA_SIGN_PROOF_GENERATION, + info: error.message, + } + }); + return; + } + rpcResponse({ ok: dataSig }); + } else { + rpcResponse({ err: 'not implemented' }); + } + }, + db, + localStorageApi, + ) + }); + } catch (e) { + handleError(e); + } + break; case 'get_balance': try { checkParamCount(1); diff --git a/packages/yoroi-extension/chrome/extension/ergo-connector/api.js b/packages/yoroi-extension/chrome/extension/ergo-connector/api.js index b647d18591..d6959192fb 100644 --- a/packages/yoroi-extension/chrome/extension/ergo-connector/api.js +++ b/packages/yoroi-extension/chrome/extension/ergo-connector/api.js @@ -15,8 +15,9 @@ import type { import { ConnectorError, TxSendErrorCodes } from './types'; import { RustModule } from '../../../app/api/ada/lib/cardanoCrypto/rustLoader'; import type { + Addressing, IGetAllUtxosResponse, - IPublicDeriver + IPublicDeriver, } from '../../../app/api/ada/lib/storage/models/PublicDeriver/interfaces'; import { PublicDeriver, } from '../../../app/api/ada/lib/storage/models/PublicDeriver/index'; import { @@ -25,6 +26,7 @@ import { asGetSigningKey, asHasLevels, asHasUtxoChains, + asGetAllAccounting, } from '../../../app/api/ada/lib/storage/models/PublicDeriver/traits'; import { ConceptualWallet } from '../../../app/api/ada/lib/storage/models/ConceptualWallet/index'; import BigNumber from 'bignumber.js'; @@ -76,6 +78,8 @@ import type { } from '../../../app/api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; import type { CardanoAddressedUtxo, } from '../../../app/api/ada/transactions/types'; import { coinSelectionForValues } from '../../../app/api/ada/transactions/shelley/coinSelection'; +import { derivePrivateByAddressing } from '../../../app/api/ada/lib/cardanoCrypto/utils'; +import { cip8Sign } from '../../../app/ergo-connector/api'; function paginateResults(results: T[], paginate: ?Paginate): T[] { if (paginate != null) { @@ -1184,8 +1188,6 @@ export async function connectorRecordSubmittedCardanoTransaction( persistSubmittedTransactions(submittedTxs); } -// TODO: generic data sign - const REORG_OUTPUT_AMOUNT = '1000000'; export async function connectorGenerateReorgTx( @@ -1245,3 +1247,103 @@ export async function connectorGenerateReorgTx( }); return { unsignedTx, collateralOutputAddressSet }; } + +export async function getAddressing( + publicDeriver: PublicDeriver<>, + address: string, +): Promise { + const findAddressing = (addresses) => { + for (const { addrs, addressing } of addresses) { + for (const { Hash } of addrs) { + if (Hash === address) { + return { addressing }; + } + } + } + }; + + const withAccounting = asGetAllAccounting(publicDeriver); + if (!withAccounting) { + throw new Error('unable to get accounting addresses from public deriver'); + } + const rewardAddressing = findAddressing( + await withAccounting.getAllAccountingAddresses(), + ); + if (rewardAddressing) { + return rewardAddressing; + } + + const withUtxos = asGetAllUtxos(publicDeriver); + if (!withUtxos) { + throw new Error('unable to get UTxO addresses from public deriver'); + } + return findAddressing( + await withUtxos.getAllUtxoAddresses(), + ); +} + +export async function connectorSignData( + publicDeriver: PublicDeriver<>, + password: string, + addressing: Addressing, + address: string, + payload: string, +): Promise<{| signature: string, key: string |}> { + const withSigningKey = asGetSigningKey(publicDeriver); + if (!withSigningKey) { + throw new Error('unable to get signing key'); + } + const normalizedKey = await withSigningKey.normalizeKey({ + ...(await withSigningKey.getSigningKey()), + password, + }); + + const withLevels = asHasLevels(publicDeriver); + if (!withLevels) { + throw new Error('unable to get levels'); + } + + const signingKey = derivePrivateByAddressing({ + addressing: addressing.addressing, + startingFrom: { + key: RustModule.WalletV4.Bip32PrivateKey.from_bytes( + Buffer.from(normalizedKey.prvKeyHex, 'hex') + ), + level: withLevels.getParent().getPublicDeriverLevel(), + }, + }).to_raw_key(); + + const coseSign1 = await cip8Sign( + Buffer.from(address, 'hex'), + signingKey, + Buffer.from(payload, 'hex'), + ); + + const key = RustModule.MessageSigning.COSEKey.new( + RustModule.MessageSigning.Label.from_key_type(RustModule.MessageSigning.KeyType.OKP) + ); + key.set_algorithm_id( + RustModule.MessageSigning.Label.from_algorithm_id(RustModule.MessageSigning.AlgorithmId.EdDSA) + ); + key.set_header( + RustModule.MessageSigning.Label.new_int( + RustModule.MessageSigning.Int.new_negative(RustModule.MessageSigning.BigNum.from_str('1')) + ), + RustModule.MessageSigning.CBORValue.new_int( + RustModule.MessageSigning.Int.new_i32(6) + ) + ); + key.set_header( + RustModule.MessageSigning.Label.new_int( + RustModule.MessageSigning.Int.new_negative(RustModule.MessageSigning.BigNum.from_str('2')) + ), + RustModule.MessageSigning.CBORValue.new_bytes( + signingKey.to_public().as_bytes() + ) + ); + + return { + signature: Buffer.from(coseSign1.to_bytes()).toString('hex'), + key: Buffer.from(key.to_bytes()).toString('hex'), + }; +} diff --git a/packages/yoroi-extension/chrome/extension/ergo-connector/types.js b/packages/yoroi-extension/chrome/extension/ergo-connector/types.js index 18e6859c67..423b0b888c 100644 --- a/packages/yoroi-extension/chrome/extension/ergo-connector/types.js +++ b/packages/yoroi-extension/chrome/extension/ergo-connector/types.js @@ -455,7 +455,7 @@ export type PendingSignData = {| type: 'data', uid: RpcUid, address: Address, - bytes: string + payload: string |} | {| type: 'tx/cardano', uid: RpcUid, @@ -476,7 +476,7 @@ export type PendingSignData = {| export type ConfirmedSignData = {| type: 'sign_confirmed', - tx: Tx | CardanoTx | CardanoTxRequest | Array, + tx: Tx | CardanoTx | CardanoTxRequest | Array | null, uid: RpcUid, tabId: number, pw: string,