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
160 changes: 132 additions & 28 deletions cli/src/commands/wallet/fund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import { requireWallet } from '../../client.ts'
import { chainCta, commandOutput, OutputContext } from '../../output.ts'
import { synapseClient } from '../../synapse.ts'

const assetOutput = z.object({
status: z.enum(['funded', 'missing', 'unconfirmed']),
balance: z.string().optional(),
txHash: z.string().optional(),
error: z.string().optional(),
})

type Asset = 'fil' | 'usdfc'
type AssetOutcome = z.infer<typeof assetOutput>

export const fundCommand = {
description: 'Request testnet FIL and USDFC from faucet (testnet only)',
mcp: {
Expand All @@ -21,49 +31,143 @@ export const fundCommand = {
}),
alias: { chain: 'c' },
output: commandOutput({
fil: z.string(),
usdfc: z.string(),
status: z.enum(['funded', 'partially_funded', 'not_funded', 'unconfirmed']),
fil: assetOutput,
usdfc: assetOutput,
faucetError: z.string().optional(),
}),
hint: 'Only works on Calibration testnet (chain 314159).',
async run(c: any) {
const out = new OutputContext(c)
if (c.options.chain !== 314159) {
return out.fail(
'TESTNET_ONLY',
'wallet fund only supports Filecoin Calibration (chain 314159). Fund mainnet wallets by sending FIL and USDFC to the wallet address.'
)
}

const blocked = requireWallet(c, out)
if (blocked) return blocked
const { client, synapse } = synapseClient(c.options.chain)
const outcomes: Record<Asset, AssetOutcome> = {
fil: { status: 'unconfirmed' },
usdfc: { status: 'unconfirmed' },
}
let faucetError: string | undefined

try {
out.step('Requesting faucet tokens')
const hashes = await claimTokens({ address: client.account.address })

out.step('Waiting for transactions to be mined')
await waitForTransactionReceipt(client, { hash: hashes[0].tx_hash })

out.step('Fetching updated balances')
const filBalance = await synapse.payments.walletBalance()
const usdfcBalance = await synapse.payments.walletBalance({
token: 'USDFC',
})

const result = {
fil: formatBalance({ value: filBalance }),
usdfc: formatBalance({ value: usdfcBalance }),
for (const transaction of hashes) {
const asset: Asset | undefined =
transaction.faucetInfo === 'CalibnetFIL'
? 'fil'
: transaction.faucetInfo === 'CalibnetUSDFC'
? 'usdfc'
: undefined
if (!asset) {
faucetError = `Unexpected faucet asset: ${transaction.faucetInfo}`
out.failStep(faucetError)
continue
}
outcomes[asset].txHash = transaction.tx_hash
try {
const receipt = await waitForTransactionReceipt(client, {
hash: transaction.tx_hash,
})
outcomes[asset].status = 'missing'
if (receipt.status !== 'success') {
outcomes[asset].error = 'Faucet transaction reverted'
out.failStep(outcomes[asset].error)
}
} catch (error) {
outcomes[asset].error = (error as Error).message
Comment thread
snissn marked this conversation as resolved.
out.failStep(outcomes[asset].error)
}
}

return out.done(result, {
cta: chainCta(c.options.chain, {
description: 'Next steps:',
commands: [
{
command: 'wallet deposit',
args: { amount: '1' },
description: 'Deposit USDFC into payment account',
},
{ command: 'wallet balance', description: 'Check balances' },
],
}),
})
} catch (error) {
return out.fail('FUND_FAILED', (error as Error).message)
faucetError = (error as Error).message
out.failStep(faucetError)
}

out.step('Fetching updated balances')
Comment thread
snissn marked this conversation as resolved.
for (const asset of ['fil', 'usdfc'] as const) {
try {
const balance =
asset === 'fil'
? await synapse.payments.walletBalance()
: await synapse.payments.walletBalance({ token: 'USDFC' })
outcomes[asset].balance = formatBalance({ value: balance })
if (outcomes[asset].status === 'missing' && !outcomes[asset].error) {
outcomes[asset].status = balance > 0n ? 'funded' : 'unconfirmed'
}
} catch (error) {
if (!outcomes[asset].error) outcomes[asset].status = 'unconfirmed'
Comment thread
snissn marked this conversation as resolved.
const balanceError = `Balance check failed: ${(error as Error).message}`
outcomes[asset].error = [outcomes[asset].error, balanceError]
.filter(Boolean)
.join('; ')
out.failStep(balanceError)
}
}

const funded = [outcomes.fil, outcomes.usdfc].filter(
({ status }) => status === 'funded'
).length
const hasUnconfirmed = [outcomes.fil, outcomes.usdfc].some(
({ status }) => status === 'unconfirmed'
)
const status = hasUnconfirmed
? 'unconfirmed'
: funded === 2
? 'funded'
: funded === 1
? 'partially_funded'
: 'not_funded'
const unavailable = (['fil', 'usdfc'] as const).filter(
(asset) => outcomes[asset].status !== 'funded'
)
const unavailableSummary = unavailable
.map((asset) => `${asset.toUpperCase()} is ${outcomes[asset].status}`)
.join(', ')
const hasMissing = unavailable.some(
(asset) => outcomes[asset].status === 'missing'
)
const recovery = [
hasMissing
? 'Request only missing assets from a documented Calibration faucet.'
: '',
hasUnconfirmed ? 'Check unconfirmed balances before retrying.' : '',
]
.filter(Boolean)
.join(' ')
const cta =
status === 'funded'
? {
description:
'Funding complete. Run wallet costs for the intended upload before depositing.',
commands: [],
}
: {
description: `Funding incomplete: ${unavailableSummary}. ${recovery}`,
commands: [
{
command: 'wallet balance',
description: 'Verify wallet balances before continuing',
},
],
}

return out.done(
{
status,
fil: outcomes.fil,
usdfc: outcomes.usdfc,
...(faucetError ? { faucetError } : {}),
},
{ cta: chainCta(c.options.chain, cta) }
)
},
}
9 changes: 9 additions & 0 deletions cli/src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ export class OutputContext {
}
}

failStep(message: string) {
if (this.log.length > 0) {
this.log[this.log.length - 1].status = 'failed'
this.log[this.log.length - 1].error = message
}
this.stopSpinner()
if (!this.agent) p.log.error(message)
}

info(message: string) {
if (!this.agent) {
this.stopSpinner()
Expand Down
10 changes: 8 additions & 2 deletions cli/tests/command-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ export const formatBalance = mock(({ value }: { value: bigint }) => {
return `formatted:${value.toString()}`
})

export const claimTokens = mock(async () => [{ tx_hash: '0xfaucet' }])
export const claimTokens = mock(async () => [
{ faucetInfo: 'CalibnetUSDFC', tx_hash: '0xusdfc' },
{ faucetInfo: 'CalibnetFIL', tx_hash: '0xfil' },
])

export const fakeProvider = {
id: 77n,
Expand Down Expand Up @@ -419,7 +422,10 @@ export function resetCommandMocks() {
formatBalance.mockImplementation(({ value }: { value: bigint }) => {
return `formatted:${value.toString()}`
})
claimTokens.mockImplementation(async () => [{ tx_hash: '0xfaucet' }])
claimTokens.mockImplementation(async () => [
{ faucetInfo: 'CalibnetUSDFC', tx_hash: '0xusdfc' },
{ faucetInfo: 'CalibnetFIL', tx_hash: '0xfil' },
])

synapsePayments.walletBalance.mockImplementation(
async (options?: { token?: string }) => (options?.token ? 2000n : 1000n)
Expand Down
Loading
Loading