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
+
+
+
+
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,