Skip to content

Commit

Permalink
Merge pull request #25 from multiversx/tm/ledger-provider
Browse files Browse the repository at this point in the history
Ledger provider
  • Loading branch information
arhtudormorar authored Sep 23, 2024
2 parents 80c0df2 + ad4c2ef commit 71b0463
Show file tree
Hide file tree
Showing 16 changed files with 367 additions and 132 deletions.
24 changes: 13 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"scripts": {
"compile": "tsc && tsc-alias",
"build-esbuild": "rimraf out && node esbuild.js",
"publish-verdaccio": "npm unpublish --registry http://localhost:4873 @multiversx/sdk-dapp-core@0.0.0-alpha.3 && rimraf out && yarn compile && npm publish --registry http://localhost:4873",
"publish-verdaccio": "npm unpublish --registry http://localhost:4873 @multiversx/sdk-dapp-core@0.0.0-alpha.11 && rimraf out && yarn compile && npm publish --registry http://localhost:4873",
"build": "yarn build-esbuild && yarn compile",
"test": "jest",
"compile-next": "rimraf out && tsc --p tsconfig.next.json && tsc-alias --project tsconfig.next.json"
Expand All @@ -32,7 +32,7 @@
"dependencies": {
"@lifeomic/axios-fetch": "3.0.1",
"@multiversx/sdk-extension-provider": "4.0.0-alpha.0",
"@multiversx/sdk-hw-provider": "6.4.0",
"@multiversx/sdk-hw-provider": "7.0.0",
"@multiversx/sdk-metamask-provider": "0.0.7",
"@multiversx/sdk-native-auth-client": "^1.0.8",
"@multiversx/sdk-opera-provider": "1.0.0-alpha.1",
Expand All @@ -41,26 +41,29 @@
"@multiversx/sdk-web-wallet-provider": "3.2.1",
"isomorphic-fetch": "3.0.0",
"lodash": "4.17.21",
"protobufjs": "7.3.0",
"socket.io-client": "4.7.5",
"zustand": "4.4.7"
},
"peerDependencies": {
"@multiversx/sdk-core": ">= 13.0.0",
"@multiversx/sdk-dapp-utils": ">= 0.1.0",
"@multiversx/sdk-web-wallet-cross-window-provider": ">= 2.0.0-alpha.1",
"@multiversx/sdk-core": ">= 13.5.0",
"@multiversx/sdk-dapp-utils": ">= 1.0.1",
"@multiversx/sdk-web-wallet-cross-window-provider": ">= 2.0.1",
"axios": ">=1.6.5",
"bignumber.js": "9.x"
"bignumber.js": "9.x",
"immer": "10.x"
},
"resolutions": {
"string-width": "4.1.0"
},
"devDependencies": {
"@types/lodash": "4.17.4",
"@multiversx/sdk-core": ">= 13.0.0",
"@multiversx/sdk-dapp-utils": ">= 0.1.0",
"@multiversx/sdk-web-wallet-cross-window-provider": ">= 2.0.0-alpha.1",
"@multiversx/sdk-core": ">= 13.5.0",
"@multiversx/sdk-dapp-utils": "1.0.1",
"@multiversx/sdk-web-wallet-cross-window-provider": ">= 2.0.1",
"@swc/core": "^1.4.17",
"@swc/jest": "^0.2.36",
"@types/jest": "29.5.13",
"@types/lodash": "4.17.4",
"@types/node": "20.12.8",
"@typescript-eslint/eslint-plugin": "7.8.0",
"@typescript-eslint/parser": "7.8.0",
Expand All @@ -84,7 +87,6 @@
"msw": "1.3.1",
"node-stdlib-browser": "1.2.0",
"prettier": "3.2.5",
"protobufjs": "^7.3.0",
"react": "^18.3.1",
"rimraf": "^5.0.6",
"ts-jest": "29.1.2",
Expand Down
7 changes: 1 addition & 6 deletions src/core/methods/signMessage/signMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,14 @@ export interface SignMessageType {
};
}

// TODO: upgrade to Message
export const signMessage = async ({
message,
callbackRoute,
options
}: SignMessageType): Promise<Nullable<Message>> => {
const address = getAddress();
const provider = getAccountProvider();
const providerType = getProviderType(provider);

const callbackUrl = addOriginToLocationPath(callbackRoute);
const messageToSign = new Message({
address: new Address(address),
data: message.data
Expand All @@ -41,9 +38,7 @@ export const signMessage = async ({
}

// TODO upgrade sdk-dapp-utils to use Message as input for signMessage method and remove the cast
const signedMessage = await provider.signMessage(messageToSign as any, {
callbackUrl: encodeURIComponent(callbackUrl)
});
const signedMessage = await provider.signMessage(messageToSign, options);

// TODO upgrade sdk-dapp-utils to return Message instead of SignableMessage and remove the cast
return signedMessage as Nullable<Message>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Transaction } from '@multiversx/sdk-core/out';

export const getAreAllTransactionsSignedByGuardian = ({
transactions,
isGuarded
}: {
transactions: Transaction[];
isGuarded?: boolean;
}) => {
if (!isGuarded) {
return true;
}

if (transactions.length === 0) {
return false;
}

return transactions.every((tx) =>
Boolean(tx.getGuardianSignature().toString('hex'))
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Transaction } from '@multiversx/sdk-core';
import { getAreAllTransactionsSignedByGuardian } from './getAreAllTransactionsSignedByGuardian';
import { ProviderFactory } from 'core/providers/ProviderFactory';
import { getAccount } from 'core/methods/account/getAccount';
import { walletAddressSelector } from 'store/selectors';
import { getState } from 'store/store';

export const getGuardedTransactions = async ({
transactions
}: {
transactions: Transaction[];
}): Promise<Transaction[]> => {
const { isGuarded, address } = getAccount();
const walletAddress = walletAddressSelector(getState());

const allSignedByGuardian = getAreAllTransactionsSignedByGuardian({
isGuarded,
transactions
});

if (!isGuarded || allSignedByGuardian) {
return transactions;
}

const factory = new ProviderFactory();
const provider = await factory.createCrossWindowProvider({
address,
walletAddress
});
provider.setShouldShowConsentPopup(true);

const guardedTransactions = await provider.guardTransactions(transactions);

return guardedTransactions;
};
13 changes: 10 additions & 3 deletions src/core/methods/signTransactions/signTransactions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
Address,
Transaction,
TransactionOptions,
TransactionVersion
} from '@multiversx/sdk-core/out';
import { getAccountProvider } from 'core/providers';
import { getAccount } from '../account/getAccount';
import { getGuardedTransactions } from './helpers/getGuardedTransactions';

type SignTransactionsOptionsType = {
skipGuardian?: boolean;
Expand All @@ -15,21 +17,26 @@ export const signTransactions = async (
options: SignTransactionsOptionsType = {}
): Promise<Transaction[]> => {
const provider = getAccountProvider();
const { isGuarded } = getAccount();
const { isGuarded, activeGuardianAddress } = getAccount();

const transacitonsToSign =
isGuarded && !options.skipGuardian
activeGuardianAddress && !options.skipGuardian
? transactions?.map((transaction) => {
transaction.setVersion(TransactionVersion.withTxOptions());
transaction.setOptions(
TransactionOptions.withOptions({ guarded: true })
);
transaction.setGuardian(Address.fromBech32(activeGuardianAddress));
return transaction;
})
: transactions;

const signedTransactions: Transaction[] =
(await provider.signTransactions(transacitonsToSign)) ?? [];

return signedTransactions;
const guardedTransactions = isGuarded
? await getGuardedTransactions({ transactions: signedTransactions })
: signedTransactions;

return guardedTransactions;
};
120 changes: 110 additions & 10 deletions src/core/providers/ProviderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
ProviderTypeEnum
} from './types/providerFactory.types';
import { isBrowserWithPopupConfirmation } from '../../constants';
import { fetchAccount } from 'utils';
import { setLedgerLogin } from 'store/actions/loginInfo/loginInfoActions';
import { setLedgerAccount } from 'store/actions/account/accountActions';
import { getLedgerProvider } from './helpers/getLedgerProvider';

export class ProviderFactory {
public async create({
Expand All @@ -17,14 +21,6 @@ export class ProviderFactory {
let createdProvider: IProvider | undefined;

switch (type) {
// case ProviderTypeEnum.iframe: {
// const provider = await ProviderFactory.getIframeProvider({
// walletAddress,
// });
// createdProvider = provider as unknown as IProvider;
// break;
// }

case ProviderTypeEnum.extension: {
const provider = await this.getExtensionProvider();
createdProvider = provider as unknown as IProvider;
Expand All @@ -39,7 +35,7 @@ export class ProviderFactory {
case ProviderTypeEnum.crossWindow: {
const { walletAddress } = config.network;

const provider = await this.getCrossWindowProvider({
const provider = await this.createCrossWindowProvider({
walletAddress,
address: config.account?.address || ''
});
Expand All @@ -52,6 +48,110 @@ export class ProviderFactory {
break;
}

case ProviderTypeEnum.ledger: {
const data = await getLedgerProvider();

if (!data) {
return;
}

const { ledgerProvider: provider, ledgerConfig } = data;

createdProvider = provider as unknown as IProvider;

const hwProviderLogin = provider.login;

createdProvider.getType = () => {
return ProviderTypeEnum.ledger;
};

createdProvider.login = async (options?: {
callbackUrl?: string | undefined;
token?: string | undefined;
}): Promise<{
address: string;
signature: string;
}> => {
const isConnected = provider.isConnected();

if (!isConnected) {
throw new Error('Ledger device is not connected');
}

// TODO: perform additional UI logic here
// maybe extract to file
const startIndex = 0;
const addressesPerPage = 10;

const accounts = await provider.getAccounts(
startIndex,
addressesPerPage
);

const accountsWithBalance: {
address: string;
balance: string;
index: number;
}[] = [];

const balancePromises = accounts.map((address) =>
fetchAccount(address)
);

const balances = await Promise.all(balancePromises);

balances.forEach((account, index) => {
if (!account) {
return;
}
accountsWithBalance.push({
address: account.address,
balance: account.balance,
index
});
});

// Suppose user selects the first account
const selectedIndex = 0;

setLedgerLogin({
index: selectedIndex,
loginType: ProviderTypeEnum.ledger
});

const { version, dataEnabled } = ledgerConfig;

setLedgerAccount({
address: accountsWithBalance[selectedIndex].address,
index: selectedIndex,
version,
hasContractDataEnabled: dataEnabled
});

if (options?.token) {
const loginInfo = await provider.tokenLogin({
token: Buffer.from(`${options?.token}{}`),
addressIndex: accountsWithBalance[selectedIndex].index
});

return {
address: loginInfo.address,
signature: loginInfo.signature.toString('hex')
};
} else {
const { address } = await hwProviderLogin({
addressIndex: accountsWithBalance[selectedIndex].index
});
return {
address,
signature: ''
};
}
};

break;
}

case ProviderTypeEnum.custom: {
createdProvider = customProvider;
break;
Expand All @@ -64,7 +164,7 @@ export class ProviderFactory {
return createdProvider;
}

private async getCrossWindowProvider({
public async createCrossWindowProvider({
address,
walletAddress
}: {
Expand Down
Loading

0 comments on commit 71b0463

Please sign in to comment.