Skip to content
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
71 changes: 71 additions & 0 deletions app-lib/accounts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
import { accountSigners } from "./accounts"

const { callMock, accountIdMock } = vi.hoisted(() => ({
callMock: vi.fn(),
accountIdMock: vi.fn(),
}))

vi.mock("./wallet", () => ({
horizon: { accounts: () => ({ accountId: accountIdMock }) },
}))

const holder = "GHOLDERACCOUNT"

describe("accountSigners", () => {
beforeEach(() => {
vi.clearAllMocks()
accountIdMock.mockReturnValue({ call: callMock })
})

it("maps the signers and thresholds of an existing account", async () => {
callMock.mockResolvedValue({
signers: [
{ key: "GSIGNERONE", weight: 1, type: "ed25519_public_key" },
{ key: "GSIGNERTWO", weight: 2, type: "ed25519_public_key" },
],
thresholds: { low_threshold: 1, med_threshold: 2, high_threshold: 3 },
})

expect(await accountSigners(holder)).toEqual({
signers: [
{ key: "GSIGNERONE", weight: 1 },
{ key: "GSIGNERTWO", weight: 2 },
],
thresholds: { low: 1, med: 2, high: 3 },
})
expect(accountIdMock).toHaveBeenCalledWith(holder)
})

it("leaves out signers that are not ed25519 keys", async () => {
callMock.mockResolvedValue({
signers: [
{ key: "GSIGNERONE", weight: 1, type: "ed25519_public_key" },
{ key: "XHASHSIGNER", weight: 1, type: "sha256_hash" },
{ key: "TPREAUTH", weight: 1, type: "preauth_tx" },
],
thresholds: { low_threshold: 0, med_threshold: 0, high_threshold: 0 },
})

const result = await accountSigners(holder)

expect(result?.signers).toEqual([{ key: "GSIGNERONE", weight: 1 }])
})

it("returns null when the account does not exist", async () => {
callMock.mockRejectedValue(new Error("Not Found"))

expect(await accountSigners(holder)).toBeNull()
})

it("returns null on a network error without throwing", async () => {
callMock.mockRejectedValue(new TypeError("Failed to fetch"))

expect(await accountSigners(holder)).toBeNull()
})

it("returns null for a contract address without asking Horizon", async () => {
expect(await accountSigners("CCONTRACTADDRESS")).toBeNull()
expect(accountIdMock).not.toHaveBeenCalled()
})
})
27 changes: 27 additions & 0 deletions app-lib/accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { horizon } from "./wallet"

export interface AccountSigners {
signers: Array<{ key: string; weight: number }>
thresholds: { low: number; med: number; high: number }
}

export async function accountSigners(
address: string,
): Promise<AccountSigners | null> {
if (!address.startsWith("G")) return null
try {
const account = await horizon.accounts().accountId(address).call()
return {
signers: account.signers
.filter((signer) => signer.type === "ed25519_public_key")
.map(({ key, weight }) => ({ key, weight })),
thresholds: {
low: account.thresholds.low_threshold,
med: account.thresholds.med_threshold,
high: account.thresholds.high_threshold,
},
}
} catch {
return null
}
}
1 change: 1 addition & 0 deletions app-lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Clients in app-lib/clients/index.ts). Framework-specific code (providers,
// stores, UI components) stays in each template.

export * from "./accounts" // AccountSigners, accountSigners
export * from "./contracts" // AsyncVaultApi, connectAsyncVault
export * from "./env" // rpcUrl, networkPassphrase, stellarNetwork, horizonUrl, network, labPrefix
export * from "./format" // shortAddress, formatNetworkName, networkStatus, NetworkState
Expand Down
6 changes: 3 additions & 3 deletions app-lib/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "@creit.tech/stellar-wallets-kit"
import { defaultModules } from "@creit.tech/stellar-wallets-kit/modules/utils"
import { Horizon } from "@stellar/stellar-sdk"
import { networkPassphrase, stellarNetwork } from "./env"
import { horizonUrl, networkPassphrase, stellarNetwork } from "./env"

// These wallets work on Local and Futurenet while the rest enforce or fail on
// any networks beside Testnet and Mainnet. Filter below based on dApp config.
Expand Down Expand Up @@ -142,7 +142,7 @@ export const onWalletChange = (
function getHorizonHost(mode: string) {
switch (mode) {
case "LOCAL":
return "http://localhost:8000"
return horizonUrl
case "FUTURENET":
return "https://horizon-futurenet.stellar.org"
case "TESTNET":
Expand All @@ -154,7 +154,7 @@ function getHorizonHost(mode: string) {
}
}

const horizon = new Horizon.Server(getHorizonHost(stellarNetwork), {
export const horizon = new Horizon.Server(getHorizonHost(stellarNetwork), {
allowHttp: stellarNetwork === "LOCAL",
})

Expand Down
3 changes: 2 additions & 1 deletion app/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ const disconnectedAuthority: AdminAuthority = {
configuration: [],
governance: [],
},
roles: [],
grants: [],
signersUnknown: false,
addresses: { oracle: null, shareToken: null, identityVerifier: null },
}

Expand Down
Loading
Loading