Skip to content

Ethers adapter update + some fixes #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 20, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/js-test/src/ethers5.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ async function start() {
// Wait for wallet SDK and account to initialize
await new Promise<void>(resolve => {
const clear = setInterval(() => {
if (window.embeddedWallet && !!window.embeddedWallet.lastAccount.address) {
if (window.embeddedWallet && !!window.embeddedWallet.lastAccount.contractAddress) {
clearInterval(clear);
resolve();
}
2 changes: 1 addition & 1 deletion apps/js-test/src/ethers6.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ async function start() {
// Wait for wallet SDK and account to initialize
await new Promise<void>(resolve => {
const clear = setInterval(() => {
if (window.embeddedWallet && !!window.embeddedWallet.lastAccount.address) {
if (window.embeddedWallet && !!window.embeddedWallet.lastAccount.contractAddress) {
clearInterval(clear);
resolve();
}
2 changes: 1 addition & 1 deletion apps/js-test/src/viem.ts
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ async function start() {
// Wait for wallet SDK and account to initialize
await new Promise<void>(resolve => {
const clear = setInterval(() => {
if (window.embeddedWallet && !!window.embeddedWallet.lastAccount.address) {
if (window.embeddedWallet && !!window.embeddedWallet.lastAccount.contractAddress) {
clearInterval(clear);
resolve();
}
2 changes: 1 addition & 1 deletion apps/vue-test/src/TestSdk.vue
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ async function transferNativeBalance() {
const res = await sendTransaction({
to: '0x700cebAA997ecAd7B0797f8f359C621604Cce6Bf',
value: '10000000',
chainId: 1287,
// chainId: 1287,
});
console.log(res);
}
4 changes: 1 addition & 3 deletions packages/gateway/src/contexts/global.context.tsx
Original file line number Diff line number Diff line change
@@ -33,9 +33,7 @@ function GlobalProvider({ children }: { children: ReactNode }) {
const urlParams = new URLSearchParams(window.location.search);

if (!wallet) {
setWallet(
EmbeddedWalletSDK({ clientId: urlParams.get('clientId') || '', noPasskeyIframe: true })
);
setWallet(EmbeddedWalletSDK({ clientId: urlParams.get('clientId') || '' }));
}

const referrer = urlParams.get('ref');
29 changes: 28 additions & 1 deletion packages/sdk/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Embedded App Wallet

Apillon Embedded Wallet JS SDK
Apillon Embedded Wallet JS SDK.

More info can be found [in the Apillon wiki](https://wiki.apillon.io/build/12-embedded-wallets-integration.html).

## Stack

@@ -34,6 +36,11 @@ defaultNetworkId?: number;
* Configuration of available networks. Oasis Sapphire is always included (ids 23294 and 23295)
*/
networks?: Network[];

/**
* Method for authenticating with passkey to make it global.
*/
passkeyAuthMode: 'redirect' | 'popup' | 'tab_process' | 'tab_form' = 'redirect';
```

The class instance is then available on window (`embeddedWallet`) and can be obtained with the `getEmbeddedWallet()` utility.
@@ -85,6 +92,18 @@ wallet.events.on('txSubmitted', tx => {

- `getAccountBalance`

- `getAccountPrivateKey`

### Wallet accounts methods

- `getAccountWallets`
Get all wallets added on user's account. Requires authentication.

- `addAccountWallet`
Add new wallet or import from privateKey.

- `updateAccountWalletTitle`

### Transaction methods

- `signMessage`
@@ -96,13 +115,21 @@ wallet.events.on('txSubmitted', tx => {
Send raw transaction data to network.
If chainId is provided, the transaction is sent to that network (cross-chain).

- `submitTransaction`
Prepare transaction and emit `txSubmitted` event (to show tx in tx history in UI e.g.).
To be used after sending transaction through anything else than `broadcastTransaction`.
Doesn't do anything by itself, just for logging/parsing transactions.

- `signContractWrite`
Get signed tx for making a contract write call.

- `contractRead`
Get result of contract read.
Utility function, this has nothing to do with Oasis.

- `processGaslessMethod`
Call a contract method with a gasless transaction (app owner pays for the transaction fees instead of user).

### mustConfirm

This parameter can be used for wallet actions that require user confirmation. If set to `true`, the event `signatureRequest`/`txApprove` will be emitted with `resolve()` method passed in payload. Once resolve is called, the action continues. This can be used to display tx confirmation UI e.g.
91 changes: 88 additions & 3 deletions packages/sdk/lib/adapters/ethers.ts
Original file line number Diff line number Diff line change
@@ -3,9 +3,8 @@ import { abort, getEmbeddedWallet } from '../utils';
import EmbeddedWallet from '..';

class EmbeddedEthersSigner extends ethers.AbstractSigner<ethers.JsonRpcProvider> {
address = '';
wallet: EmbeddedWallet;
// override provider: ethers.JsonRpcProvider;
internalSigner: InternalEmbeddedEthersSignerextends;

constructor(provider?: ethers.JsonRpcProvider) {
const w = getEmbeddedWallet();
@@ -16,8 +15,94 @@ class EmbeddedEthersSigner extends ethers.AbstractSigner<ethers.JsonRpcProvider>

super(provider || w.getRpcProviderForChainId(w.defaultNetworkId));

this.internalSigner = new InternalEmbeddedEthersSignerextends(
provider || w.getRpcProviderForChainId(w.defaultNetworkId),
w
);

this.wallet = w!;
// this.provider = provider;

/**
* Reinitialize signer with new provider when chain changes
*/
w.events.on('dataUpdated', ({ name, newValue }) => {
if (name === 'defaultNetworkId') {
this.internalSigner = new InternalEmbeddedEthersSignerextends(
w.getRpcProviderForChainId(newValue),
this.wallet
);
}
});
}

override connect(): ethers.Signer {
return this.internalSigner;
}

override async getAddress(): Promise<string> {
const a = await this.wallet.getAccountAddress();
return a || '';
}

override async signTransaction(
tx: ethers.TransactionRequest,
mustConfirm = true
): Promise<string> {
return this.internalSigner.signTransaction(tx, mustConfirm);
}

override async signMessage(message: string | Uint8Array, mustConfirm = true): Promise<string> {
return this.internalSigner.signMessage(message, mustConfirm);
}

override async sendTransaction(
tx: ethers.TransactionRequest
): Promise<ethers.TransactionResponse> {
return this.internalSigner.sendTransaction(tx);
}

/**
* NOT implemented
*/
override async signTypedData(
domain: ethers.TypedDataDomain,
types: Record<string, ethers.TypedDataField[]>,
value: Record<string, any>
): Promise<string> {
console.error('EmbeddedEthersSigner#signTypedData Not implemented', { domain, types, value });
return '';
}

/**
* @deprecated v5 signer properties
*/
_isSigner = true;
async getBalance(blockTag?: ethers.BlockTag) {
return this.internalSigner.getBalance(blockTag);
}
async getTransactionCount(blockTag?: ethers.BlockTag) {
return this.internalSigner.getTransactionCount(blockTag);
}
async getChainId() {
return this.internalSigner.getChainId();
}
async getGasPrice() {
return this.internalSigner.getGasPrice();
}
async getFeeData() {
return this.internalSigner.getFeeData();
}
}

class InternalEmbeddedEthersSignerextends extends ethers.AbstractSigner<ethers.JsonRpcProvider> {
// address = '';
// override provider: ethers.JsonRpcProvider;

constructor(
provider: ethers.JsonRpcProvider,
private wallet: EmbeddedWallet
) {
super(provider);
}

override connect(): ethers.Signer {
43 changes: 41 additions & 2 deletions packages/sdk/lib/index.ts
Original file line number Diff line number Diff line change
@@ -495,7 +495,7 @@ class EmbeddedWallet {
funcDataTypes = 'tuple(bytes32 hashedUsername, bytes32 digest, bytes data)';
}

const res = await this.gaslessTx({
const res = await this.processGaslessMethod({
label: 'Add new account',
strategy: params.strategy,
authData: params.authData,
@@ -1028,6 +1028,35 @@ class EmbeddedWallet {
// return receipt;
}

/**
* Prepare tx and emit `txSubmitted` event (to show tx in tx history in UI e.g.)
*/
submitTransaction(
txHash: string,
signedTxData?: ethers.BytesLike,
chainId?: number,
label = 'Transaction',
internalLabel?: string
) {
const txItem = {
hash: txHash,
label,
rawData: signedTxData || '',
owner: this.lastAccount.wallets[this.lastAccount.walletIndex].address || 'none',
status: 'pending' as const,
chainId: chainId || this.defaultNetworkId,
explorerUrl: this.explorerUrls[chainId || this.defaultNetworkId]
? `${this.explorerUrls[chainId || this.defaultNetworkId]}/tx/${txHash}`
: '',
createdAt: Date.now(),
internalLabel,
} as TransactionItem;

this.events.emit('txSubmitted', txItem);

return txItem;
}

/**
* Get signed tx for making a contract write call.
*/
@@ -1164,7 +1193,17 @@ class EmbeddedWallet {
}
}

async gaslessTx(params: {
/**
* Call an `Account Manager` contract method with a gasless transaction.
* This means that app owner (clientId) pays for the transaction fees instead of user.
* These methods must be supported by `generateGaslessTx` method on the contract.
* Supported methods are defined by `GaslessTxType`.
* About
* - get & confirm credentials
* - calculate and format tx data (according to `funcDataTypes` and `funcDataValuesFormatter` params)
* - broadcast the tx (marked with `label` from params)
*/
async processGaslessMethod(params: {
strategy: AuthStrategyName;
authData: AuthData;
data: any;
13 changes: 4 additions & 9 deletions packages/sdk/lib/types.ts
Original file line number Diff line number Diff line change
@@ -14,9 +14,9 @@ export type Network = {
id: number;
rpcUrl: string;
explorerUrl: string;
imageUrl?: string;
currencySymbol?: string;
currencDecimals?: number;
imageUrl?: string; // Icon of the chain for display in UI
currencySymbol?: string; // Symbol of the native currency (default is 'ETH')
currencyDecimals?: number; // Number of decimals of the native currency (default is 18)
};

export type SignatureCallback = (
@@ -52,14 +52,9 @@ export type AppParams = {
networks?: Network[];

/**
* Use a new window for creating and authenticating with a passkey
* Method for authenticating with passkey to make it global.
*/
passkeyAuthMode?: AuthPasskeyMode;

/**
* Iframe uses same wallet code. Set this to prevent implosion.
*/
noPasskeyIframe?: boolean;
};

export type AuthData = {
5 changes: 1 addition & 4 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
@@ -4,12 +4,9 @@ This package provides default UI for showing the state of connected account and

Use `EmbeddedWalletUI()` to initialize SDK and the UI. The UI is done with React and HeadlessUI (tailwind).

There are some UI specific options in addition to all SDK options.
There are some UI specific options in addition to all the SDK options.

```ts
// Supported networks info, for showing names and links to explorer.
networks?: { name: string; id: number; rpcUrl: string; explorerUrl: string }[];

/**
* Automatically broadcast with SDK after confirming a transaction.
*
2 changes: 1 addition & 1 deletion packages/ui/src/components/Approve/ApproveButtons.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { useWalletContext } from '../../contexts/wallet.context';
import clsx from 'clsx';

export default ({
doneAfterApprove = false,
doneAfterApprove = true,
doneAfterDecline = true,
className,
onApprove,
9 changes: 3 additions & 6 deletions packages/ui/src/components/Approve/ApproveContractTx.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useApproveContext } from '../../contexts/approve.context';
import { useWalletContext } from '../../contexts/wallet.context';
import { formatTxObjectData } from '../../lib/helpers';
import ApproveButtons from './ApproveButtons';
import ApproveDataRow from './ApproveDataRow';

@@ -25,7 +26,7 @@ export default () => {
{!!contractFunctionData.chainId && !!networksById[contractFunctionData.chainId] && (
<ApproveDataRow
label="Chain"
data={networksById[contractFunctionData.chainId].name}
data={`${contractFunctionData.chainId} (${networksById[contractFunctionData.chainId].name})`}
className="mb-4"
/>
)}
@@ -46,11 +47,7 @@ export default () => {
!!contractFunctionData.contractFunctionValues.length && (
<ApproveDataRow
label="Contract function values"
data={JSON.stringify(
contractFunctionData.contractFunctionValues,
(_, value) => (typeof value === 'bigint' ? value.toString() : value),
2
)}
data={formatTxObjectData(contractFunctionData.contractFunctionValues)}
className="mb-4"
collapsable
/>
42 changes: 30 additions & 12 deletions packages/ui/src/components/Approve/ApprovePlainTx.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import { useApproveContext } from '../../contexts/approve.context';
import { useWalletContext } from '../../contexts/wallet.context';
import { formatTxObjectData } from '../../lib/helpers';
import ApproveButtons from './ApproveButtons';
import ApproveDataRow from './ApproveDataRow';

const ExcludedTxKeys = ['data', 'gasLimit', 'nonce', 'maxFeePerGas', 'gasPrice'];
const ExcludedTxKeys = [
'data',
'gasLimit',
'nonce',
'maxFeePerGas',
'gasPrice',
'maxFeePerBlobGas',
'maxPriorityFeePerGas',
'chain', // viem
'blobs', // viem
'accessList', // viem
'authorizationList', // viem
'nonceManager', // viem
'type', // viem
];

export default () => {
const {
wallet,
state: { username, appProps },
formatNativeBalance,
networksById,
} = useWalletContext();

const {
@@ -31,26 +47,28 @@ export default () => {
return true;
}

if (k === 'value' && !v) {
return false;
}

return !ExcludedTxKeys.includes(k);
})
.map(([k, v]) => (
<ApproveDataRow
key={k}
label={k}
data={
k === 'value'
k === 'value' || k === 'gas'
? formatNativeBalance(v)
: typeof v === 'bigint'
? v.toString()
: typeof v === 'object'
? JSON.stringify(
v,
(_, value) => (typeof value === 'bigint' ? value.toString() : value),
2
)
: v
: k === 'chainId'
? `${v}${networksById[v] ? ` (${networksById[v].name})` : ''}`
: typeof v === 'bigint'
? v.toString()
: typeof v === 'object'
? formatTxObjectData(v)
: v
}
collapsable={typeof v === 'object'}
collapsable={typeof v === 'object' || k === 'data'}
/>
))}
</div>
2 changes: 1 addition & 1 deletion packages/ui/src/contexts/tokens.context.tsx
Original file line number Diff line number Diff line change
@@ -98,7 +98,7 @@ function TokensProvider({ children }: { children: React.ReactNode }) {
address: '',
name: `${networksById?.[walletState.networkId]?.name} ETH`,
symbol: networksById?.[walletState.networkId]?.currencySymbol || 'ETH',
decimals: networksById?.[walletState.networkId]?.currencDecimals || 18,
decimals: networksById?.[walletState.networkId]?.currencyDecimals || 18,
balance: activeWallet?.balance || '',
}),
[activeWallet?.balance, walletState.networkId]
2 changes: 1 addition & 1 deletion packages/ui/src/contexts/wallet.context.tsx
Original file line number Diff line number Diff line change
@@ -403,7 +403,7 @@ function WalletProvider({

function formatNativeBalance(balance: string | bigint | number) {
return (
ethers.formatUnits(balance, networksById?.[state.networkId]?.currencDecimals || 18) +
ethers.formatUnits(balance, networksById?.[state.networkId]?.currencyDecimals || 18) +
` ${networksById?.[state.networkId]?.currencySymbol || 'ETH'}`
);
}
13 changes: 13 additions & 0 deletions packages/ui/src/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -23,3 +23,16 @@ export function logToStorage(msg: string) {
export function formatBalance(balance: string, unit = 'ETH') {
return `${parseFloat(balance)} ${unit}`;
}

/**
* Format object/array data for display on approve screens
*/
export function formatTxObjectData(data: Object) {
if (Array.isArray(data)) {
return data.join(`\n`);
}

return Object.values(data).reduce((acc, [key, value]) => {
acc += `${key}: ${value}`;
}, '');
}