@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
347 lines (342 loc) • 11.9 kB
JavaScript
import { Buffer as _Buffer } from "buffer";
import _defineProperty from "@babel/runtime-corejs3/helpers/defineProperty";
import nacl from 'tweetnacl';
import AeSdk from './AeSdk.js';
import verifyTransaction from './tx/validator.js';
import RpcClient from './aepp-wallet-communication/rpc/RpcClient.js';
import { METHODS, RPC_STATUS, SUBSCRIPTION_TYPES, RpcInvalidTransactionError, RpcNotAuthorizeError, RpcPermissionDenyError, RpcUnsupportedProtocolError } from './aepp-wallet-communication/schema.js';
import { InternalError, UnknownRpcClientError } from './utils/errors.js';
import { RPC_VERSION } from './aepp-wallet-communication/rpc/types.js';
import { Encoding, encode, decode } from './utils/encoder.js';
import jsonBig from './utils/json-big.js';
/**
* Contain functionality for aepp interaction and managing multiple aepps
* @category aepp wallet communication
*/
export default class AeSdkWallet extends AeSdk {
/**
* @param options - Options
* @param options.name - Wallet name
* @param options.id - Wallet id
* @param options.type - Wallet type
* @param options.onConnection - Call-back function for incoming AEPP connection
* @param options.onSubscription - Call-back function for incoming AEPP account subscription
* @param options.onAskAccounts - Call-back function for incoming AEPP get address request
* @param options.onAskToSelectNetwork - Call-back function for incoming AEPP select network
* request. If the request is fine then this function should change the current network.
* @param options.onDisconnect - Call-back function for disconnect event
*/
constructor({
name,
id,
type,
onConnection,
onSubscription,
onDisconnect,
onAskAccounts,
onAskToSelectNetwork,
...options
}) {
super(options);
_defineProperty(this, "_clients", new Map());
this.onConnection = onConnection;
this.onSubscription = onSubscription;
this.onDisconnect = onDisconnect;
this.onAskAccounts = onAskAccounts;
this.onAskToSelectNetwork = onAskToSelectNetwork;
this.name = name;
this.id = id;
this._type = type;
}
_getAccountsForClient({
addressSubscription
}) {
const {
current,
connected
} = this.getAccounts();
return {
current: addressSubscription.has('current') || addressSubscription.has('connected') ? current : {},
connected: addressSubscription.has('connected') ? connected : {}
};
}
_pushAccountsToApps() {
if (this._clients == null) return;
Array.from(this._clients.keys()).filter(clientId => this._isRpcClientConnected(clientId)).map(clientId => this._getClient(clientId)).filter(client => client.addressSubscription.size !== 0).forEach(client => client.rpc.notify(METHODS.updateAddress, this._getAccountsForClient(client)));
}
selectAccount(address) {
super.selectAccount(address);
this._pushAccountsToApps();
}
addAccount(account, options) {
super.addAccount(account, options);
this._pushAccountsToApps();
}
_getNode() {
this.ensureNodeConnected();
return {
node: {
url: this.api.$host,
name: this.selectedNodeName
}
};
}
async selectNode(name) {
super.selectNode(name);
const networkId = await this.api.getNetworkId();
Array.from(this._clients.keys()).filter(clientId => this._isRpcClientConnected(clientId)).map(clientId => this._getClient(clientId)).forEach(client => {
client.rpc.notify(METHODS.updateNetwork, {
networkId,
...(client.connectNode && this._getNode())
});
});
}
_getClient(clientId) {
const client = this._clients.get(clientId);
if (client == null) throw new UnknownRpcClientError(clientId);
return client;
}
_isRpcClientConnected(clientId) {
return RPC_STATUS.CONNECTED === this._getClient(clientId).status && this._getClient(clientId).rpc.connection.isConnected();
}
_disconnectRpcClient(clientId) {
const client = this._getClient(clientId);
client.rpc.connection.disconnect();
client.status = RPC_STATUS.DISCONNECTED;
client.addressSubscription = new Set();
}
/**
* Remove specific RpcClient by ID
* @param id - Client ID
*/
removeRpcClient(id) {
this._disconnectRpcClient(id);
this._clients.delete(id);
}
/**
* Add new client by AEPP connection
* @param clientConnection - AEPP connection object
* @returns Client ID
*/
addRpcClient(clientConnection) {
// @TODO detect if aepp has some history based on origin????
// if yes use this instance for connection
const id = _Buffer.from(nacl.randomBytes(8)).toString('base64');
let disconnectParams;
const client = {
id,
status: RPC_STATUS.WAITING_FOR_CONNECTION_REQUEST,
addressSubscription: new Set(),
connectNode: false,
rpc: new RpcClient(clientConnection, () => {
this._clients.delete(id);
this.onDisconnect(id, disconnectParams); // also related info
}, {
[METHODS.closeConnection]: params => {
disconnectParams = params;
this._disconnectRpcClient(id);
},
// Store client info and prepare two fn for each client `connect` and `denyConnection`
// which automatically prepare and send response for that client
[METHODS.connect]: async ({
name,
version,
icons,
connectNode
}, origin) => {
if (version !== RPC_VERSION) throw new RpcUnsupportedProtocolError();
await this.onConnection(id, {
name,
icons,
connectNode
}, origin);
client.status = RPC_STATUS.CONNECTED;
client.connectNode = connectNode;
return {
...(await this.getWalletInfo()),
...(connectNode && this._getNode())
};
},
[METHODS.subscribeAddress]: async ({
type,
value
}, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
switch (type) {
case SUBSCRIPTION_TYPES.subscribe:
// TODO: remove `type` as it always subscribe
await this.onSubscription(id, {
type,
value
}, origin);
client.addressSubscription.add(value);
break;
case SUBSCRIPTION_TYPES.unsubscribe:
client.addressSubscription.delete(value);
break;
default:
throw new InternalError(`Unknown subscription type: ${type}`);
}
return {
subscription: Array.from(client.addressSubscription),
address: this._getAccountsForClient(client)
};
},
[METHODS.address]: async (params, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
await this.onAskAccounts(id, params, origin);
return this.addresses();
},
[METHODS.sign]: async ({
tx,
onAccount = this.address,
returnSigned,
innerTx
}, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
if (!this.addresses().includes(onAccount)) {
throw new RpcPermissionDenyError(onAccount);
}
const parameters = {
onAccount,
aeppOrigin: origin,
aeppRpcClientId: id,
innerTx
};
if (returnSigned || innerTx === true) {
return {
signedTransaction: await this.signTransaction(tx, parameters)
};
}
try {
return jsonBig.parse(jsonBig.stringify({
transactionHash: await this.sendTransaction(tx, {
...parameters,
verify: false
})
}));
} catch (error) {
const validation = await verifyTransaction(tx, this.api);
if (validation.length > 0) throw new RpcInvalidTransactionError(validation);
throw error;
}
},
[METHODS.signMessage]: async ({
message,
onAccount = this.address
}, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
if (!this.addresses().includes(onAccount)) {
throw new RpcPermissionDenyError(onAccount);
}
const parameters = {
onAccount,
aeppOrigin: origin,
aeppRpcClientId: id
};
return {
signature: _Buffer.from(await this.signMessage(message, parameters)).toString('hex')
};
},
[METHODS.signTypedData]: async ({
domain,
aci,
data,
onAccount = this.address
}, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
if (!this.addresses().includes(onAccount)) {
throw new RpcPermissionDenyError(onAccount);
}
const parameters = {
...domain,
onAccount,
aeppOrigin: origin,
aeppRpcClientId: id
};
return {
signature: await this.signTypedData(data, aci, parameters)
};
},
[METHODS.unsafeSign]: async ({
data,
onAccount = this.address
}, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
if (!this.addresses().includes(onAccount)) throw new RpcPermissionDenyError(onAccount);
const parameters = {
onAccount,
aeppOrigin: origin,
aeppRpcClientId: id
};
const signature = encode(await this.unsafeSign(decode(data), parameters), Encoding.Signature);
return {
signature
};
},
[METHODS.signDelegation]: async ({
delegation,
onAccount = this.address
}, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
if (!this.addresses().includes(onAccount)) throw new RpcPermissionDenyError(onAccount);
const parameters = {
onAccount,
aeppOrigin: origin,
aeppRpcClientId: id
};
const signature = await this.signDelegation(delegation, parameters);
return {
signature
};
},
[METHODS.updateNetwork]: async (params, origin) => {
if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError();
await this.onAskToSelectNetwork(id, params, origin);
return null;
}
})
};
this._clients.set(id, client);
return id;
}
/**
* Send shareWalletInfo message to notify AEPP about wallet
* @param clientId - ID of RPC client send message to
*/
async shareWalletInfo(clientId) {
this._getClient(clientId).rpc.notify(METHODS.readyToConnect, await this.getWalletInfo());
}
/**
* Get Wallet info object
* @returns Object with wallet information
*/
async getWalletInfo() {
const {
origin
} = window.location;
return {
id: this.id,
name: this.name,
networkId: await this.api.getNetworkId(),
origin: origin === 'file://' ? '*' : origin,
type: this._type
};
}
/**
* Get Wallet accounts
* @returns Object with accounts information (\{ connected: Object, current: Object \})
*/
getAccounts() {
return {
current: this.selectedAddress != null ? {
[this.selectedAddress]: {}
} : {},
connected: this.addresses().filter(a => a !== this.selectedAddress).reduce((acc, a) => ({
...acc,
[a]: {}
}), {})
};
}
}
//# sourceMappingURL=AeSdkWallet.js.map