Skip to content
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

Add Some Public & API Functions #1

Merged
merged 20 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Account Variables
PK=

# Test Variables
VITE_TEST_NETWORK_TYPE='ETHEREUM_SEPOLIA'
VITE_EXAMPLE_SAFE='SHORTNAME:ADDRESS' # sep:0xbcA814Ee6E571d0BbC335b4e3869F89E532Ba8B8
VITE_TEST_ADDRESS= # sep:0xD7a0ca30F71cFDF45534B058c567a5FaE6C33846
VITE_ANVIL_FORK_URL=https://eth-sepolia.g.alchemy.com/v2/[KEY]
VITE_ANVIL_BLOCK_NUMBER=5211612
5 changes: 3 additions & 2 deletions src/actions/public/estimateSafeTransactionGas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import { estimateSafeTransactionGas } from './estimateSafeTransactionGas.js'
import { publicClient } from '../../../tests/utils.js'
import { zeroAddress } from 'viem'
import { EIP3770Address, OperationType } from '../../types.js'
import { EXAMPLE_SAFE } from '../../../tests/constants.js'

describe('estimateSafeTransactionGas', () => {
it('should estimate fixed gas on a eth transfer tx', async () => {
const gas1 = await estimateSafeTransactionGas(publicClient, process.env.VITE_SAFE_ADDRESS as EIP3770Address, {
const gas1 = await estimateSafeTransactionGas(publicClient, EXAMPLE_SAFE as EIP3770Address, {
to: zeroAddress,
value: 1n,
operation: OperationType.Call
})
const gas2 = await estimateSafeTransactionGas(publicClient, process.env.VITE_SAFE_ADDRESS as EIP3770Address, {
const gas2 = await estimateSafeTransactionGas(publicClient, EXAMPLE_SAFE as EIP3770Address, {
to: zeroAddress,
value: 8n,
operation: OperationType.Call
Expand Down
3 changes: 2 additions & 1 deletion src/actions/public/getSafeNonce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { describe, it, expect } from 'vitest'
import { getSafeNonce } from './getSafeNonce.js'
import { publicClient } from '../../../tests/utils.js'
import { EIP3770Address } from '../../types.js'
import { EXAMPLE_SAFE } from '../../../tests/constants.js'

describe('getSafeNonce', () => {
it('should retrieve the current nonce', () => {
getSafeNonce(publicClient, process.env.VITE_SAFE_ADDRESS as EIP3770Address).then((nonce) => {
getSafeNonce(publicClient, EXAMPLE_SAFE as EIP3770Address).then((nonce) => {
expect(nonce).toBeTypeOf('bigint')
})
})
Expand Down
2 changes: 2 additions & 0 deletions src/actions/public/getSafeNonce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { safeAbi } from '../../constants.js'
import { ArgsWithChainId, EIP3770Address } from '../../types.js'
import { getEip3770Address } from '../../utils/eip-3770.js'

// TODO Add a seperate method for getNextNonce which handles pending transactions

export const getSafeNonce = async (
client: PublicClient,
safeAddress: EIP3770Address | Address,
Expand Down
14 changes: 14 additions & 0 deletions src/actions/public/getSafeOwners.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, it, expect } from 'vitest'
import { getSafeOwners } from './getSafeOwners.js'
import { publicClient } from '../../../tests/utils.js'
import { EIP3770Address } from '../../types.js'
import { EXAMPLE_SAFE, TEST_ADDRESS } from '../../../tests/constants.js'
import { parseEip3770Address } from '../../utils/eip-3770.js'

describe('getSafeOwners', () => {
it('should retrieve the safes owners', () => {
getSafeOwners(publicClient, EXAMPLE_SAFE as EIP3770Address).then((owners) => {
expect(owners).includes(parseEip3770Address(TEST_ADDRESS).address)
})
})
})
18 changes: 18 additions & 0 deletions src/actions/public/getSafeOwners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Address, PublicClient } from 'viem'
import { safeAbi } from '../../constants.js'
import { ArgsWithChainId, EIP3770Address } from '../../types.js'
import { getEip3770Address } from '../../utils/eip-3770.js'

export const getSafeOwners = async (
client: PublicClient,
safeAddress: EIP3770Address | Address,
args: ArgsWithChainId = {},
): Promise<Address[]> => {
const { address } = getEip3770Address({ fullAddress: safeAddress, chainId: args.chainId || client.chain!.id })

return await client.readContract({
abi: safeAbi,
functionName: 'getOwners',
address
}) as Address[]
}
7 changes: 5 additions & 2 deletions src/actions/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { EstimateSafeTransactionGasArgs, estimateSafeTransactionGas } from './es
import { GetSafeTransactionHashArgs, getSafeTransactionHash } from './getSafeTransactionHash.js'
import { EstimateSafeTransactionBaseGasArgs, estimateSafeTransactionBaseGas } from './estimateSafeTransactionBaseGas.js'
import { getSafeNonce } from './getSafeNonce.js'
import { getSafeOwners } from './getSafeOwners.js'
import { ArgsWithChainId, EIP3770Address } from '../../types.js'

export type PublicSafeActions = {
estimateSafeTransactionGas: (args: EstimateSafeTransactionGasArgs) => Promise<bigint>
getSafeTransactionHash: (args: GetSafeTransactionHashArgs) => Promise<Hash>
estimateSafeTransactionBaseGas: (args: EstimateSafeTransactionBaseGasArgs) => Promise<bigint>
getSafeNonce: (args?: ArgsWithChainId) => Promise<bigint>
getSafeOwners: (args?: ArgsWithChainId) => Promise<Address[]>
}

export const publicSafeActions = (
Expand All @@ -19,10 +21,11 @@ export const publicSafeActions = (
estimateSafeTransactionGas: (args) => estimateSafeTransactionGas(client, safeAddress, args),
getSafeTransactionHash: (args) => getSafeTransactionHash(client, safeAddress, args),
estimateSafeTransactionBaseGas: (args) => estimateSafeTransactionBaseGas(client, safeAddress, args),
getSafeNonce: (args) => getSafeNonce(client, safeAddress, args)
getSafeNonce: (args) => getSafeNonce(client, safeAddress, args),
getSafeOwners: (args) => getSafeOwners(client, safeAddress, args)
})
}

export {
estimateSafeTransactionBaseGas, estimateSafeTransactionGas, getSafeTransactionHash, getSafeNonce
estimateSafeTransactionBaseGas, estimateSafeTransactionGas, getSafeTransactionHash, getSafeNonce, getSafeOwners
}
109 changes: 83 additions & 26 deletions src/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest'
import { ApiClient } from './api.js'
import { goerli } from 'viem/chains'

const EXAMPLE_SAFE = 'gor:0x04786B39Bd84b3a5344dC7355e4d8785b0981902'
import { sepolia, goerli } from 'viem/chains'
import { TEST_ADDRESS, EXAMPLE_SAFE_ADDRESS, EXAMPLE_SAFE } from '../tests/constants.js'
import { EXAMPLE_SAFE_INFO_RESPONSE, MULTISIG_TRANSACTION_TEST_RESPONSE_0, MULTISIG_TRANSACTION_TEST_RESPONSE_1, NONCE_0, TEST_TRANSACTION_HASH_0 } from '../tests/test-data.js'

describe('ApiClient', () => {
it('initializes properly', () => {
Expand All @@ -11,28 +11,85 @@ describe('ApiClient', () => {
expect(api.safeAddress).toEqual(EXAMPLE_SAFE)
expect(api.chainId).toEqual(1)
})
})

describe('getDelegates', () => {
it('should list all delegates', async () => {
const api = new ApiClient({ url: 'https://safe-transaction-goerli.safe.global', chainId: goerli.id, safeAddress: EXAMPLE_SAFE })

const result = await api.getDelegates()

expect(result).toEqual(
{
count: 1,
next: null,
previous: null,
results: [
{
safe: '0x04786B39Bd84b3a5344dC7355e4d8785b0981902',
delegate: '0x54bCee87c3397AF31a92b8faf27faB77758a27Ce',
delegator: '0xcC528c1bC5F43D15C0b9DC41b510E764Dc19da4D',
label: 'Delegate'
}
]
}
)
describe('getSafeInfo', () => {
it('should return the safe info', async () => {
const api = new ApiClient({ url: 'https://safe-transaction-sepolia.safe.global', chainId: sepolia.id, safeAddress: EXAMPLE_SAFE })
const result = await api.getSafeInfo(EXAMPLE_SAFE)

expect(result).toEqual(EXAMPLE_SAFE_INFO_RESPONSE)
})
})

describe('getTransaction', () => {
it('should return a transaction for a given tx hash', async () => {
const api = new ApiClient({ url: 'https://safe-transaction-sepolia.safe.global', chainId: sepolia.id, safeAddress: EXAMPLE_SAFE })
const result = await api.getTransaction(TEST_TRANSACTION_HASH_0)

expect(result).toEqual(MULTISIG_TRANSACTION_TEST_RESPONSE_0)
})
})
})

describe('getMultisigTransactions', () => {
it('should return a list of transactions for a given safe address', async () => {
const api = new ApiClient({ url: 'https://safe-transaction-sepolia.safe.global', chainId: sepolia.id, safeAddress: EXAMPLE_SAFE })
const result = await api.getMultisigTransactions(EXAMPLE_SAFE)

const firstTx = result?.results.find((tx) => tx.nonce == NONCE_0)
expect(firstTx).toEqual(MULTISIG_TRANSACTION_TEST_RESPONSE_0)
})
})

describe('getPendingTransactions', () => {
it('should return a list of pending transactions', async () => {
const api = new ApiClient({ url: 'https://safe-transaction-sepolia.safe.global', chainId: sepolia.id, safeAddress: EXAMPLE_SAFE })

const result = await api.getPendingTransactions({ safeAddress: EXAMPLE_SAFE })

expect(result).toEqual(
{
count: 1,
next: null,
previous: null,
results: [MULTISIG_TRANSACTION_TEST_RESPONSE_1],
countUniqueNonce: 2
})
})
})

// TODO convert to sepolia safe
describe.skip('getDelegates', () => {
it('should list all delegates', async () => {
const api = new ApiClient({ url: 'https://safe-transaction-goerli.safe.global', chainId: goerli.id, safeAddress: 'gor:0x04786B39Bd84b3a5344dC7355e4d8785b0981902' })

const result = await api.getDelegates()

expect(result).toEqual(
{
count: 1,
next: null,
previous: null,
results: [
{
safe: '0x04786B39Bd84b3a5344dC7355e4d8785b0981902',
delegate: '0x54bCee87c3397AF31a92b8faf27faB77758a27Ce',
delegator: '0xcC528c1bC5F43D15C0b9DC41b510E764Dc19da4D',
label: 'Delegate'
}
]
}
)
})
})

describe('getSafesByOwner', () => {
it('should list all safes owned by an address', async () => {
const api = new ApiClient({ url: 'https://safe-transaction-sepolia.safe.global', chainId: sepolia.id, safeAddress: EXAMPLE_SAFE })

const result = await api.getSafesByOwner(TEST_ADDRESS)

expect(result.safes).includes(EXAMPLE_SAFE_ADDRESS)
})
})
})

94 changes: 90 additions & 4 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Address, Hex, Hash } from 'viem'
import type { EIP3770Address, SafeTransactionData, SafeTransactionDataPartial } from './types.js'
import type { EIP3770Address, SafeInfoResponse, SafeMultisigTransactionListResponse, SafeMultisigTransactionResponse, SafeTransactionData, SafeTransactionDataPartial, SignatureResponse } from './types.js'
import { getEip3770Address } from './utils/eip-3770.js'

Object.defineProperty(BigInt.prototype, 'toJSON', {
Expand Down Expand Up @@ -67,14 +67,19 @@ export async function sendRequest<T>({
}

export type ProposeTransactionProps = {
safeTransactionData: Omit<SafeTransactionData, 'value' |'data'> & Pick<SafeTransactionDataPartial, 'value' | 'data'>
safeTransactionData: Omit<SafeTransactionData, 'value' | 'data'> & Pick<SafeTransactionDataPartial, 'value' | 'data'>
safeTxHash: Hash
senderAddress: `${string}:${Address}` | Address
senderSignature: Hex
origin?: string
chainId?: number
}

export type GetPendingTransactionsProps = {
safeAddress: EIP3770Address | Address,
currentNonce?: number
}

export type GetDelegateProps = Partial<{
delegateAddress: EIP3770Address | Address
delegatorAddress: EIP3770Address | Address
Expand All @@ -96,6 +101,10 @@ export type SafeDelegateListResponse = {
}[]
}

export type SafesByOwnersResponse = {
readonly safes: Address[]
}

export class ApiClient {
#url: URL | string
safeAddress: EIP3770Address | Address
Expand All @@ -105,6 +114,19 @@ export class ApiClient {
this.safeAddress = safeAddress
this.chainId = chainId
}
async getSafeInfo(safeAddress: EIP3770Address | Address): Promise<SafeInfoResponse> {
if (!safeAddress) throw new Error('Invalid Safe address')

const { address } = getEip3770Address({
fullAddress: safeAddress,
chainId: this.chainId
})

return sendRequest({
url: `${this.#url}/v1/safes/${address}/`,
method: 'GET'
})
}
async proposeTransaction({
safeTransactionData,
senderAddress,
Expand Down Expand Up @@ -137,14 +159,67 @@ export class ApiClient {
to,
value: safeTransactionData.value ?? 0n
}


return sendRequest({
url: `${this.#url}/v1/safes/${safeAddress}/multisig-transactions/`,
method: 'POST',
body
})
}
async confirmTransaction(safeTxHash: string, signature: Hex): Promise<SignatureResponse> {
if (!safeTxHash) throw new Error('Invalid safeTxHash')

if (!signature) throw new Error('Invalid signature')

return sendRequest({
url: `${this.#url}/v1/multisig-transactions/${safeTxHash}/confirmations/`,
method: 'POST',
body: { signature }
})
}
async getTransaction(safeTxHash: string): Promise<SafeMultisigTransactionResponse> {
return sendRequest({
url: `${this.#url}/v1/multisig-transactions/${safeTxHash}/`,
method: 'GET'
})
}
async getMultisigTransactions(safeAddress: EIP3770Address | Address): Promise<SafeMultisigTransactionListResponse> {
if (!safeAddress) throw new Error('Invalid Safe address')

const { address } = getEip3770Address({
fullAddress: safeAddress,
chainId: this.chainId
})

return sendRequest({
url: `${this.#url}/v1/safes/${address}/multisig-transactions/`,
method: 'GET'
})
}
async getPendingTransactions({
safeAddress,
currentNonce
}: GetPendingTransactionsProps): Promise<SafeMultisigTransactionListResponse[]> {
if (!safeAddress) throw new Error('Invalid Safe address')

const { address } = getEip3770Address({
fullAddress: safeAddress,
chainId: this.chainId
})

const nonce = currentNonce ?? (await this.getSafeInfo(address)).nonce

const url = new URL(`${this.#url}/v1/safes/${address}/multisig-transactions/`)

url.searchParams.set('executed', 'false')
url.searchParams.set('nonce__gte', nonce.toString())

return sendRequest({
url: url.toString(),
method: 'GET'
})
}
async getDelegates(args?: GetDelegateProps): Promise<SafeDelegateListResponse> {

const { delegateAddress,
Expand All @@ -161,7 +236,7 @@ export class ApiClient {
})

url.searchParams.set('safe', safeAddress)

if (delegateAddress) {
const { address: delegate } = getEip3770Address({ fullAddress: delegateAddress, chainId: chainId ?? this.chainId })
url.searchParams.set('delegate', delegate)
Expand All @@ -184,4 +259,15 @@ export class ApiClient {
method: 'GET'
})
}
async getSafesByOwner(ownerAddress: EIP3770Address | Address): Promise<SafesByOwnersResponse> {
const { address: owner } = getEip3770Address({
fullAddress: ownerAddress,
chainId: this.chainId
})

return sendRequest({
url: `${this.#url}/v1/owners/${owner}/safes`,
method: 'GET'
})
}
}
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const simulateTxAccessorAbi = [
] as const

export const safeAbi = parseAbi([
'function simulateAndRevert(address targetContract, bytes memory calldataPayload)', 'function getTransactionHash(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce) public view returns (bytes32)', 'function execTransaction(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes memory signatures) public payable returns (bool)', 'function nonce() public view returns (uint256)', 'function getThreshold() public view returns (uint256)'
'function simulateAndRevert(address targetContract, bytes memory calldataPayload)', 'function getTransactionHash(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce) public view returns (bytes32)', 'function execTransaction(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes memory signatures) public payable returns (bool)', 'function nonce() public view returns (uint256)', 'function getThreshold() public view returns (uint256)', 'function getOwners() public view returns (address[])'
])

export const simulateTxAccessorAddress = '0x59AD6735bCd8152B84860Cb256dD9e96b85F69Da'
Expand Down
Loading
Loading