This guide shows how to build either an WebExtension Wallet or a iFrame-based Wallet.
The full implementation of this example can be found here:
Note:
- If you want to see a more advanced implementation you can take a look into the repository of the Superhero Wallet
First you need to create a bridge between your extension and the page. This can be done as follows:
import browser from 'webextension-polyfill'; | |
import { | |
BrowserRuntimeConnection, | |
BrowserWindowMessageConnection, | |
MESSAGE_DIRECTION, | |
connectionProxy, | |
} from '@aeternity/aepp-sdk'; | |
(async () => { | |
console.log('Waiting until document is ready'); | |
await new Promise((resolve) => { | |
const interval = setInterval(() => { | |
// TODO: ensure that there is no corresponding event | |
if (document.readyState !== 'complete') return; | |
clearInterval(interval); | |
resolve(); | |
}, 100); | |
}); | |
console.log('Document is ready'); | |
const port = browser.runtime.connect(); | |
const extConnection = new BrowserRuntimeConnection({ port }); | |
const pageConnection = new BrowserWindowMessageConnection({ | |
target: window, | |
...(window.origin !== 'null' && { origin: window.origin }), | |
sendDirection: MESSAGE_DIRECTION.to_aepp, | |
receiveDirection: MESSAGE_DIRECTION.to_waellet, | |
}); | |
connectionProxy(pageConnection, extConnection); | |
})(); |
Then you need to initialize AeSdkWallet
class in your extension and subscribe for new runtime
connections.
After the connection is established you can share the wallet details with the application.
aepp-sdk-js/examples/browser/wallet-web-extension/src/background.js
Lines 1 to 163 in 568c291
import browser from 'webextension-polyfill'; | |
import { | |
AeSdkWallet, | |
CompilerHttp, | |
Node, | |
MemoryAccount, | |
BrowserRuntimeConnection, | |
WALLET_TYPE, | |
RpcConnectionDenyError, | |
RpcRejectedByUserError, | |
RpcNoNetworkById, | |
unpackTx, | |
unpackDelegation, | |
} from '@aeternity/aepp-sdk'; | |
import { TypeResolver, ContractByteArrayEncoder } from '@aeternity/aepp-calldata'; | |
function stringifyBigint(value) { | |
return JSON.stringify(value, (k, v) => (typeof v === 'bigint' ? `${v} (as BigInt)` : v), 2); | |
} | |
let popupCounter = 0; | |
async function confirmInPopup(parameters) { | |
const popupUrl = new URL(browser.runtime.getURL('./popup.html')); | |
const popupId = popupCounter; | |
popupCounter += 1; | |
popupUrl.searchParams.set('data', stringifyBigint({ ...parameters, popupId })); | |
await browser.windows.create({ | |
url: popupUrl.toString(), | |
type: 'popup', | |
height: 600, | |
width: 600, | |
}); | |
return new Promise((resolve) => { | |
const handler = (message, sender, sendResponse) => { | |
if (message.popupId !== popupId) return; | |
resolve(message.response); | |
sendResponse(); | |
browser.runtime.onMessage.removeListener(handler); | |
}; | |
browser.runtime.onMessage.addListener(handler); | |
}); | |
} | |
const aeppInfo = {}; | |
const genConfirmCallback = (action) => async (aeppId, parameters, aeppOrigin) => { | |
const isConfirmed = await confirmInPopup({ | |
...parameters, | |
action, | |
aeppId, | |
aeppInfo: aeppInfo[aeppId], | |
aeppOrigin, | |
}); | |
if (!isConfirmed) throw new RpcRejectedByUserError(); | |
}; | |
class AccountMemoryProtected extends MemoryAccount { | |
async signTransaction(transaction, { aeppRpcClientId: id, aeppOrigin, ...options } = {}) { | |
if (id != null) { | |
const opt = { ...options, transaction, unpackedTx: unpackTx(transaction) }; | |
if (opt.onCompiler) opt.onCompiler = '<Compiler>'; | |
if (opt.onNode) opt.onNode = '<Node>'; | |
await genConfirmCallback('sign transaction')(id, opt, aeppOrigin); | |
} | |
return super.signTransaction(transaction, options); | |
} | |
async signMessage(message, { aeppRpcClientId: id, aeppOrigin, ...options } = {}) { | |
if (id != null) { | |
await genConfirmCallback('sign message')(id, { ...options, message }, aeppOrigin); | |
} | |
return super.signMessage(message, options); | |
} | |
async signTypedData(data, aci, { aeppRpcClientId: id, aeppOrigin, ...options }) { | |
if (id != null) { | |
const dataType = new TypeResolver().resolveType(aci); | |
const decodedData = new ContractByteArrayEncoder().decodeWithType(data, dataType); | |
const opt = { | |
...options, | |
aci, | |
data, | |
decodedData, | |
}; | |
await genConfirmCallback('sign typed data')(id, opt, aeppOrigin); | |
} | |
return super.signTypedData(data, aci, options); | |
} | |
async sign(data, { aeppRpcClientId: id, aeppOrigin, ...options } = {}) { | |
if (id != null) { | |
await genConfirmCallback(`sign raw data ${data}`)(id, options, aeppOrigin); | |
} | |
return super.sign(data, options); | |
} | |
async signDelegation(delegation, { aeppRpcClientId: id, aeppOrigin, ...options }) { | |
if (id != null) { | |
const opt = { ...options, ...unpackDelegation(delegation) }; | |
await genConfirmCallback('sign delegation')(id, opt, aeppOrigin); | |
} | |
return super.signDelegation(delegation, options); | |
} | |
static generate() { | |
return new AccountMemoryProtected(super.generate().secretKey); | |
} | |
} | |
const aeSdk = new AeSdkWallet({ | |
onCompiler: new CompilerHttp('https://v8.compiler.aepps.com'), | |
nodes: [ | |
{ name: 'ae_uat', instance: new Node('https://testnet.aeternity.io') }, | |
{ name: 'ae_mainnet', instance: new Node('https://mainnet.aeternity.io') }, | |
], | |
accounts: [ | |
new AccountMemoryProtected('sk_2CuofqWZHrABCrM7GY95YSQn8PyFvKQadnvFnpwhjUnDCFAWmf'), | |
AccountMemoryProtected.generate(), | |
], | |
id: browser.runtime.id, | |
type: WALLET_TYPE.extension, | |
name: 'Wallet WebExtension', | |
async onConnection(aeppId, params, aeppOrigin) { | |
const isConfirmed = await confirmInPopup({ | |
action: 'connect', | |
aeppId, | |
aeppInfo: params, | |
aeppOrigin, | |
}); | |
if (!isConfirmed) throw new RpcConnectionDenyError(); | |
aeppInfo[aeppId] = params; | |
}, | |
onDisconnect(aeppId, payload) { | |
console.log('Client disconnected:', aeppId, payload); | |
}, | |
onSubscription: genConfirmCallback('subscription'), | |
onAskAccounts: genConfirmCallback('get accounts'), | |
async onAskToSelectNetwork(aeppId, parameters, origin) { | |
await genConfirmCallback('select network')(aeppId, parameters, origin); | |
if (parameters.networkId) { | |
if (!this.pool.has(parameters.networkId)) throw new RpcNoNetworkById(parameters.networkId); | |
await this.selectNode(parameters.networkId); | |
} else { | |
this.pool.delete('by-aepp'); | |
this.addNode('by-aepp', new Node(parameters.nodeUrl)); | |
await this.selectNode('by-aepp'); | |
} | |
}, | |
}); | |
// The `ExtensionProvider` uses the first account by default. | |
// You can change active account using `selectAccount(address)` function | |
browser.runtime.onConnect.addListener((port) => { | |
// create connection | |
const connection = new BrowserRuntimeConnection({ port }); | |
// add new aepp to wallet | |
const clientId = aeSdk.addRpcClient(connection); | |
// share wallet details | |
aeSdk.shareWalletInfo(clientId); | |
const interval = setInterval(() => aeSdk.shareWalletInfo(clientId), 3000); | |
port.onDisconnect.addListener(() => clearInterval(interval)); | |
}); | |
console.log('Wallet initialized!'); |
The iFrame-based approach works similar to the WebExtension approach except that the connectionProxy
in between isn't needed.
You can take a look into the implementation of the following example to see how it works: