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
79 changes: 14 additions & 65 deletions app-lib/contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,27 @@ vi.mock("@stellar/stellar-sdk/contract", async (importOriginal) => {

import {
connectAsyncVault,
connectIdentityVerifier,
connectNavOracle,
connectShareToken,
type Signer,
} from "./contracts"

describe("connectAsyncVault", () => {
describe.each([
{ name: "connectAsyncVault", connect: connectAsyncVault },
{ name: "connectNavOracle", connect: connectNavOracle },
{ name: "connectShareToken", connect: connectShareToken },
{ name: "connectIdentityVerifier", connect: connectIdentityVerifier },
] satisfies {
name: string
connect: (contractId: string, signer?: Signer) => Promise<unknown>
}[])("$name", ({ connect }) => {
beforeEach(() => fromMock.mockClear())

it("builds a read client with no publicKey or signTransaction when no signer is given", async () => {
fromMock.mockResolvedValue({})

await connectAsyncVault("CCONTRACT")
await connect("CCONTRACT")

const options = fromMock.mock.calls[0]?.[0]
expect(options.contractId).toBe("CCONTRACT")
Expand All @@ -32,71 +42,10 @@ describe("connectAsyncVault", () => {
fromMock.mockResolvedValue({})
const signTransaction = vi.fn()

await connectAsyncVault("CCONTRACT", {
publicKey: "GADDRESS",
signTransaction,
})
await connect("CCONTRACT", { publicKey: "GSIGNER", signTransaction })

const options = fromMock.mock.calls[0]?.[0]
expect(options.publicKey).toBe("GADDRESS")
expect(options.signTransaction).toBe(signTransaction)
})
})

describe("connectNavOracle", () => {
beforeEach(() => fromMock.mockClear())

it("builds a read client with no publicKey or signTransaction when no signer is given", async () => {
fromMock.mockResolvedValue({})

await connectNavOracle("CORACLE")

const options = fromMock.mock.calls[0]?.[0]
expect(options.contractId).toBe("CORACLE")
expect(options.publicKey).toBeUndefined()
expect(options.signTransaction).toBeUndefined()
})

it("carries the connected address and signer when one is given", async () => {
fromMock.mockResolvedValue({})
const signTransaction = vi.fn()

await connectNavOracle("CORACLE", {
publicKey: "GATTESTER",
signTransaction,
})

const options = fromMock.mock.calls[0]?.[0]
expect(options.publicKey).toBe("GATTESTER")
expect(options.signTransaction).toBe(signTransaction)
})
})

describe("connectShareToken", () => {
beforeEach(() => fromMock.mockClear())

it("builds a read client with no publicKey or signTransaction when no signer is given", async () => {
fromMock.mockResolvedValue({})

await connectShareToken("CTOKEN")

const options = fromMock.mock.calls[0]?.[0]
expect(options.contractId).toBe("CTOKEN")
expect(options.publicKey).toBeUndefined()
expect(options.signTransaction).toBeUndefined()
})

it("carries the connected address and signer when one is given", async () => {
fromMock.mockResolvedValue({})
const signTransaction = vi.fn()

await connectShareToken("CTOKEN", {
publicKey: "GTOKENADMIN",
signTransaction,
})

const options = fromMock.mock.calls[0]?.[0]
expect(options.publicKey).toBe("GTOKENADMIN")
expect(options.publicKey).toBe("GSIGNER")
expect(options.signTransaction).toBe(signTransaction)
})
})
3 changes: 2 additions & 1 deletion app-lib/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ export const connectNavOracle = (

export const connectIdentityVerifier = (
contractId: string,
signer?: Signer,
): Promise<IdentityVerifierApi> =>
Client.from<IdentityVerifierApi>(clientOptions(contractId))
Client.from<IdentityVerifierApi>(clientOptions(contractId, signer))

export const connectShareToken = (
contractId: string,
Expand Down
18 changes: 18 additions & 0 deletions app-lib/format.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest"
import { networkPassphrase } from "./env"
import {
addAmounts,
AMOUNT_DECIMALS,
type Amount,
formatDate,
Expand All @@ -15,6 +16,7 @@ import {
PRICE_DECIMALS,
type Price,
shortAddress,
subtractAmounts,
} from "./format"

describe("shortAddress", () => {
Expand Down Expand Up @@ -100,6 +102,22 @@ describe("parseUnits", () => {
})
})

describe("addAmounts", () => {
it("adds two amounts", () => {
expect(addAmounts(5n as Amount, 7n as Amount)).toBe(12n)
})
})

describe("subtractAmounts", () => {
it("subtracts the second amount from the first", () => {
expect(subtractAmounts(12n as Amount, 7n as Amount)).toBe(5n)
})

it("floors at zero instead of going negative", () => {
expect(subtractAmounts(7n as Amount, 12n as Amount)).toBe(0n)
})
})

describe("parseAmount", () => {
it("strips commas and parses a positive number", () => {
expect(parseAmount("1,234.50")).toBe(1234.5)
Expand Down
5 changes: 5 additions & 0 deletions app-lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ export function parseUnits<D extends Decimals>(
return (negative ? -magnitude : magnitude) as Scaled<D>
}

export const addAmounts = (a: Amount, b: Amount): Amount => (a + b) as Amount

export const subtractAmounts = (a: Amount, b: Amount): Amount =>
(a > b ? a - b : 0n) as Amount

export const parseAmount = (raw: string): number | null => {
const value = Number(raw.replace(/,/g, ""))
return Number.isFinite(value) && value > 0 ? value : null
Expand Down
159 changes: 159 additions & 0 deletions app-lib/readContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
readEpochs,
readEvents,
readLedgerTime,
readContractAdmin,
readPendingAdmins,
} from "./readContract"

Expand Down Expand Up @@ -266,6 +267,26 @@ describe("readEpochRequests", () => {
expect(result.get(5n)?.epoch).toEqual({ kind: "archived" })
})

it("maps epoch and request entries reporting a TTL of 0 to archived", async () => {
getLedgerEntriesMock.mockResolvedValue({
entries: [
entryFor(EPOCH_5_KEY_XDR, openEpochScVal, 0),
entryFor(DEPOSIT_5_KEY_XDR, depositScVal, 0),
entryFor(REDEEM_5_KEY_XDR, redeemScVal, 0),
],
latestLedger: 100,
})
getLatestLedgerMock.mockResolvedValue({ sequence: 100 })

const result = await readEpochRequests(CONTRACT_ID, CONTROLLER, [5n])

expect(result.get(5n)).toEqual({
epoch: { kind: "archived" },
deposit: { kind: "archived" },
redeem: { kind: "archived" },
})
})

it("marks a value unreadable when the stored ScVal does not match the expected struct shape", async () => {
getLedgerEntriesMock.mockResolvedValue({
entries: [entryFor(EPOCH_5_KEY_XDR, xdr.ScVal.scvBool(true), 1000)],
Expand Down Expand Up @@ -366,6 +387,18 @@ describe("readEpochs", () => {
expect(result.get(5n)).toEqual({ kind: "archived" })
})

it("reports an entry whose TTL is 0 as archived", async () => {
getLedgerEntriesMock.mockResolvedValue({
entries: [entryFor(EPOCH_5_KEY_XDR, openEpochScVal, 0)],
latestLedger: 100,
})
getLatestLedgerMock.mockResolvedValue({ sequence: 100 })

const result = await readEpochs(CONTRACT_ID, [5n])

expect(result.get(5n)).toEqual({ kind: "archived" })
})

it("packs 201 epochs into two requests of 200 and 1", async () => {
getLedgerEntriesMock.mockResolvedValue({ entries: [], latestLedger: 100 })
getLatestLedgerMock.mockResolvedValue({ sequence: 100 })
Expand Down Expand Up @@ -775,6 +808,18 @@ describe("readPendingAdmins", () => {
)
})

it("reads an entry whose TTL is 0 as no transfer", async () => {
getLedgerEntriesMock.mockResolvedValue({
entries: [pendingEntry(CONTRACT_ID, 1_500, 0)],
})

const read = await readPendingAdmins([CONTRACT_ID])

expect(read.kind === "value" && read.value.pending.get(CONTRACT_ID)).toBe(
null,
)
})

it.each([
["a value that is not a transfer", xdr.ScVal.scvU32(7)],
[
Expand Down Expand Up @@ -812,3 +857,117 @@ describe("readPendingAdmins", () => {
})
})
})

const instanceKey = (contractId: string): xdr.LedgerKey =>
xdr.LedgerKey.contractData(
new xdr.LedgerKeyContractData({
contract: Address.fromString(contractId).toScAddress(),
key: xdr.ScVal.scvLedgerKeyContractInstance(),
durability: xdr.ContractDataDurability.persistent(),
}),
)

const instanceEntry = (
storage: xdr.ScMapEntry[],
liveUntilLedgerSeq = 2_000,
): Api.LedgerEntryResult =>
entryFor(
instanceKey(CONTRACT_ID).toXDR("base64"),
xdr.ScVal.scvContractInstance(
new xdr.ScContractInstance({
executable: xdr.ContractExecutable.contractExecutableStellarAsset(),
storage,
}),
),
liveUntilLedgerSeq,
)

const storageEntry = (name: string, val: xdr.ScVal) =>
new xdr.ScMapEntry({
key: xdr.ScVal.scvVec([xdr.ScVal.scvSymbol(name)]),
val,
})

describe("readContractAdmin", () => {
beforeEach(() => {
getLedgerEntriesMock.mockReset()
getLatestLedgerMock.mockReset()
getLatestLedgerMock.mockResolvedValue({ sequence: 1_000 })
})

it("reads the admin from the contract instance's storage", async () => {
getLedgerEntriesMock.mockResolvedValue({
entries: [
instanceEntry([
storageEntry("PendingAdmin", xdr.ScVal.scvU32(1)),
storageEntry("Admin", Address.fromString(PENDING).toScVal()),
]),
],
})

expect(await readContractAdmin(CONTRACT_ID)).toEqual({
kind: "value",
value: PENDING,
})
const sent = getLedgerEntriesMock.mock.calls[0] as xdr.LedgerKey[]
expect(sent.map((key) => key.toXDR("base64"))).toEqual([
instanceKey(CONTRACT_ID).toXDR("base64"),
])
})

it("reads the admin from an instance whose TTL the RPC reports as 0", async () => {
getLedgerEntriesMock.mockResolvedValue({
entries: [
instanceEntry(
[storageEntry("Admin", Address.fromString(PENDING).toScVal())],
0,
),
],
})

expect(await readContractAdmin(CONTRACT_ID)).toEqual({
kind: "value",
value: PENDING,
})
})

it("reports an instance whose TTL has lapsed as archived", async () => {
getLedgerEntriesMock.mockResolvedValue({
entries: [
instanceEntry(
[storageEntry("Admin", Address.fromString(PENDING).toScVal())],
999,
),
],
})

expect(await readContractAdmin(CONTRACT_ID)).toEqual({ kind: "archived" })
})

it("reads no admin when the instance holds none", async () => {
getLedgerEntriesMock.mockResolvedValue({ entries: [instanceEntry([])] })

expect(await readContractAdmin(CONTRACT_ID)).toEqual({
kind: "value",
value: null,
})
})

it("reports a malformed admin as unreadable", async () => {
getLedgerEntriesMock.mockResolvedValue({
entries: [instanceEntry([storageEntry("Admin", xdr.ScVal.scvU32(7))])],
})

expect(await readContractAdmin(CONTRACT_ID)).toEqual({
kind: "unreadable",
})
})

it("reports a failed ledger read as unreadable", async () => {
getLedgerEntriesMock.mockRejectedValue(new Error("rpc down"))

expect(await readContractAdmin(CONTRACT_ID)).toEqual({
kind: "unreadable",
})
})
})
Loading
Loading