safe-apps-sdk
Version:
SDK developed to integrate third-party apps with Safe app.
196 lines (162 loc) • 5.53 kB
text/typescript
import { ethers } from 'ethers';
import { EIP_1271_INTERFACE, EIP_1271_BYTES_INTERFACE, MAGIC_VALUE_BYTES, MAGIC_VALUE } from './signatures';
import { Methods } from '../communication/methods';
import { RPC_CALLS } from '../eth/constants';
import {
Communicator,
SafeInfo,
ChainInfo,
SafeBalances,
GetBalanceParams,
RPCPayload,
TransactionConfig,
EnvironmentInfo,
AddressBookItem,
isObjectEIP712TypedData,
EIP712TypedData,
} from '../types';
import requirePermission from '../decorators/requirePermissions';
class Safe {
private readonly communicator: Communicator;
constructor(communicator: Communicator) {
this.communicator = communicator;
}
async getChainInfo(): Promise<ChainInfo> {
const response = await this.communicator.send<Methods.getChainInfo, undefined, ChainInfo>(
Methods.getChainInfo,
undefined,
);
return response.data;
}
async getInfo(): Promise<SafeInfo> {
const response = await this.communicator.send<Methods.getSafeInfo, undefined, SafeInfo>(
Methods.getSafeInfo,
undefined,
);
return response.data;
}
// There is a possibility that this method will change because we may add pagination to the endpoint
async experimental_getBalances({ currency = 'usd' }: GetBalanceParams = {}): Promise<SafeBalances> {
const response = await this.communicator.send<Methods.getSafeBalances, { currency: string }, SafeBalances>(
Methods.getSafeBalances,
{
currency,
},
);
return response.data;
}
private async check1271Signature(messageHash: string, signature = '0x'): Promise<boolean> {
const safeInfo = await this.getInfo();
const encodedIsValidSignatureCall = EIP_1271_INTERFACE.encodeFunctionData('isValidSignature', [
messageHash,
signature,
]);
const payload = {
call: RPC_CALLS.eth_call,
params: [
{
to: safeInfo.safeAddress,
data: encodedIsValidSignatureCall,
},
'latest',
],
};
try {
const response = await this.communicator.send<Methods.rpcCall, RPCPayload<[TransactionConfig, string]>, string>(
Methods.rpcCall,
payload,
);
return response.data.slice(0, 10).toLowerCase() === MAGIC_VALUE;
} catch (err) {
return false;
}
}
private async check1271SignatureBytes(messageHash: string, signature = '0x'): Promise<boolean> {
const safeInfo = await this.getInfo();
const msgBytes = ethers.utils.arrayify(messageHash);
const encodedIsValidSignatureCall = EIP_1271_BYTES_INTERFACE.encodeFunctionData('isValidSignature', [
msgBytes,
signature,
]);
const payload = {
call: RPC_CALLS.eth_call,
params: [
{
to: safeInfo.safeAddress,
data: encodedIsValidSignatureCall,
},
'latest',
],
};
try {
const response = await this.communicator.send<Methods.rpcCall, RPCPayload<[TransactionConfig, string]>, string>(
Methods.rpcCall,
payload,
);
return response.data.slice(0, 10).toLowerCase() === MAGIC_VALUE_BYTES;
} catch (err) {
return false;
}
}
calculateMessageHash(message: string): string {
return ethers.utils.hashMessage(message);
}
calculateTypedMessageHash(typedMessage: EIP712TypedData): string {
return ethers.utils._TypedDataEncoder.hash(typedMessage.domain, typedMessage.types, typedMessage.message);
}
async getOffChainSignature(messageHash: string): Promise<string> {
const response = await this.communicator.send<Methods.getOffChainSignature, string, string>(
Methods.getOffChainSignature,
messageHash,
);
return response.data;
}
async isMessageSigned(message: string | EIP712TypedData, signature = '0x'): Promise<boolean> {
let check: (() => Promise<boolean>) | undefined;
if (typeof message === 'string') {
check = async (): Promise<boolean> => {
const messageHash = this.calculateMessageHash(message);
const messageHashSigned = await this.isMessageHashSigned(messageHash, signature);
return messageHashSigned;
};
}
if (isObjectEIP712TypedData(message)) {
check = async (): Promise<boolean> => {
const messageHash = this.calculateTypedMessageHash(message);
const messageHashSigned = await this.isMessageHashSigned(messageHash, signature);
return messageHashSigned;
};
}
if (check) {
const isValid = await check();
return isValid;
}
throw new Error('Invalid message type');
}
async isMessageHashSigned(messageHash: string, signature = '0x'): Promise<boolean> {
const checks = [this.check1271Signature.bind(this), this.check1271SignatureBytes.bind(this)];
for (const check of checks) {
const isValid = await check(messageHash, signature);
if (isValid) {
return true;
}
}
return false;
}
async getEnvironmentInfo(): Promise<EnvironmentInfo> {
const response = await this.communicator.send<Methods.getEnvironmentInfo, undefined, EnvironmentInfo>(
Methods.getEnvironmentInfo,
undefined,
);
return response.data;
}
async requestAddressBook(): Promise<AddressBookItem[]> {
const response = await this.communicator.send<Methods.requestAddressBook, undefined, AddressBookItem[]>(
Methods.requestAddressBook,
undefined,
);
return response.data;
}
}
export { Safe };