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
29 changes: 27 additions & 2 deletions layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,25 @@
variant="outlined"
class="mb-4"
/>
<v-text-field
v-model="funderWalletApiKey"
label="Funder Wallet API Key (Delegate funding only)"
variant="outlined"
type="password"
class="mb-4"
/>
<v-text-field
v-model="funderEntitySecret"
label="Funder Entity Secret (Delegate funding only)"
variant="outlined"
type="password"
class="mb-4"
/>
<p class="subtitle-2 font-weight-light">
Configure your Circle Developer Controlled Wallets credentials
for signing operations. Funder Wallet ID is used for the
funder permit when signing in delegate funding mode.
for signing operations. Funder credentials are used for the
funder permit when signing in delegate funding mode (the LLC
funder wallet is typically under a separate entity).
</p>
</v-form>
</v-expansion-panel-text>
Expand Down Expand Up @@ -740,6 +755,16 @@ const funderWalletId = computed({
set: (value: string) => store.setFunderWalletId(value),
})

const funderWalletApiKey = computed({
get: () => store.getFunderWalletApiKey,
set: (value: string) => store.setFunderWalletApiKey(value),
})

const funderEntitySecret = computed({
get: () => store.getFunderEntitySecret,
set: (value: string) => store.setFunderEntitySecret(value),
})

const riskSignalIpAddress = computed({
get: () => store.getRiskSignals.ipAddress,
set: (value: string) =>
Expand Down
58 changes: 28 additions & 30 deletions pages/debug/stablefx/fund.vue
Original file line number Diff line number Diff line change
Expand Up @@ -323,40 +323,38 @@ const fundDelegateBatch = async () => {
}
batchFundResults.value = results

let anySuccess = false

for (const entry of delegateBatch.value) {
try {
const fundPayload: StableFXFundPayload = {
type: formData.type as 'maker' | 'taker',
signature: entry.traderSignature,
fundingMode: 'delegate',
permit2: entry.traderTypedData?.message,
funderPermit2: entry.funderTypedData?.message,
funderSignature: entry.funderSignature,
}
await $stablefxTradesApi.fund(fundPayload)
batchFundResults.value = {
...batchFundResults.value,
[entry.contractTradeId]: 'success',
}
anySuccess = true
} catch (err) {
batchFundResults.value = {
...batchFundResults.value,
[entry.contractTradeId]: 'error',
}
error.value = err
showError.value = true
// Both delegate and net_delegate: the batch permit covers all trades in a single API call.
// funding-presign stores the same shared typed data on every entry regardless of mode,
// so one fund call using the first entry is correct for all cases.
const entry = delegateBatch.value[0]
try {
const fundPayload: StableFXFundPayload = {
type: formData.type as 'maker' | 'taker',
signature: entry.traderSignature,
fundingMode: formData.fundingMode as 'delegate' | 'net_delegate',
permit2: entry.traderTypedData?.message,
funderPermit2: entry.funderTypedData?.message,
funderSignature: entry.funderSignature,
}
}

if (anySuccess) {
await $stablefxTradesApi.fund(fundPayload)
const succeeded: Record<string, 'success' | 'error' | 'pending'> = {}
for (const e of delegateBatch.value) {
succeeded[e.contractTradeId] = 'success'
}
batchFundResults.value = succeeded
fundingSuccess.value = true
store.clearDelegateFundingBatch()
} catch (err) {
const failed: Record<string, 'success' | 'error' | 'pending'> = {}
for (const e of delegateBatch.value) {
failed[e.contractTradeId] = 'error'
}
batchFundResults.value = failed
error.value = err
showError.value = true
} finally {
loading.value = false
}

loading.value = false
}

const goToGetTrades = () => {
Expand Down
83 changes: 46 additions & 37 deletions pages/debug/stablefx/funding-presign.vue
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@
</v-list-item-title>
<template #append>
<v-chip
v-if="entry.traderSignature && entry.funderSignature"
v-if="
!!entry.funderSignature &&
(!entry.traderTypedData || !!entry.traderSignature)
"
color="success"
size="small"
>
Expand Down Expand Up @@ -274,7 +277,8 @@ const allDelegateSigned = computed(
() =>
delegateBatch.value.length > 0 &&
delegateBatch.value.every(
(e: DelegateFundingEntry) => !!e.traderSignature && !!e.funderSignature,
(e: DelegateFundingEntry) =>
(!e.traderTypedData || !!e.traderSignature) && !!e.funderSignature,
),
)

Expand Down Expand Up @@ -348,16 +352,18 @@ const makeApiCall = async () => {
const resp =
await $stablefxTradesApi.getFundingPresignData(presignPayload)
const data = (resp as any)?.data ?? resp
const results: any[] = Array.isArray(data) ? data : [data]
const batch: DelegateFundingEntry[] = tradeIds.map(
(tradeId: string, idx: number) => ({
contractTradeId: tradeId,
traderTypedData: results[idx]?.traderPermitTypedData ?? null,
funderTypedData: results[idx]?.funderPermitTypedData ?? null,
traderSignature: '',
funderSignature: '',
}),
)
const traderTypedData =
data?.traderPermitTypedData ?? data?.batchTraderPermitTypedData ?? null
const funderTypedData =
data?.funderPermitTypedData ?? data?.batchFunderPermitTypedData ?? null
// All entries share the same typed data — one batch signature covers all trades
const batch: DelegateFundingEntry[] = tradeIds.map((tradeId: string) => ({
contractTradeId: tradeId,
traderTypedData,
funderTypedData,
traderSignature: '',
funderSignature: '',
}))
store.setDelegateFundingBatch(batch)
} else {
const presignPayload: FundingPresignPayload = {
Expand Down Expand Up @@ -401,42 +407,45 @@ const signWithCircle = async () => {
formData.fundingMode === 'net_delegate'
) {
const batch = [...store.getDelegateFundingBatch]
const total = batch.length * 2

for (let i = 0; i < batch.length; i++) {
const entry = { ...batch[i] }

signingProgressText.value = `Signing trader permit ${i + 1} of ${batch.length}...`
signingProgress.value = Math.round(((i * 2) / total) * 100)

const funderWalletId = store.getFunderWalletId || store.getWalletId
const funderApiKey = store.getFunderWalletApiKey || store.getWalletApiKey
const funderSecret = store.getFunderEntitySecret || store.getEntitySecret
const firstEntry = batch[0]
let traderSignature = ''

if (firstEntry.traderTypedData) {
signingProgressText.value = 'Signing trader permit...'
signingProgress.value = 0
const traderResult = await $circleWalletsApi.signTypedDataComplete(
store.getWalletId,
JSON.stringify(entry.traderTypedData),
JSON.stringify(firstEntry.traderTypedData),
store.getEntitySecret,
store.getWalletApiKey,
)
entry.traderSignature =
traderSignature =
traderResult?.data?.signature || traderResult?.signature || ''
signingProgress.value = 50
}

signingProgressText.value = `Signing funder permit ${i + 1} of ${batch.length}...`
signingProgress.value = Math.round(((i * 2 + 1) / total) * 100)

const funderWalletId = store.getFunderWalletId || store.getWalletId
const funderResult = await $circleWalletsApi.signTypedDataComplete(
funderWalletId,
JSON.stringify(entry.funderTypedData),
store.getEntitySecret,
store.getWalletApiKey,
)
entry.funderSignature =
funderResult?.data?.signature || funderResult?.signature || ''
signingProgressText.value = 'Signing funder permit...'
const funderResult = await $circleWalletsApi.signTypedDataComplete(
funderWalletId,
JSON.stringify(firstEntry.funderTypedData),
funderSecret,
funderApiKey,
)
const funderSignature =
funderResult?.data?.signature || funderResult?.signature || ''

batch[i] = entry
}
const updatedBatch = batch.map((entry) => ({
...entry,
traderSignature,
funderSignature,
}))

signingProgress.value = 100
signingProgressText.value = 'All signatures collected.'
store.setDelegateFundingBatch(batch)
store.setDelegateFundingBatch(updatedBatch)
} else {
let typedData = response.value.data?.typedData || response.value.typedData
if (!typedData && response.value.data) {
Expand Down
26 changes: 26 additions & 0 deletions stores/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ interface MainState {
entitySecret: string
walletId: string
funderWalletId: string
funderWalletApiKey: string
funderEntitySecret: string
riskSignals: RiskSignals
apiRequest: ApiRequest
delegateFundingBatch: DelegateFundingEntry[]
Expand Down Expand Up @@ -53,6 +55,14 @@ export const useMainStore = defineStore('main', {
typeof window !== 'undefined'
? localStorage.getItem('funderWalletId') || ''
: '',
funderWalletApiKey:
typeof window !== 'undefined'
? localStorage.getItem('funderWalletApiKey') || ''
: '',
funderEntitySecret:
typeof window !== 'undefined'
? localStorage.getItem('funderEntitySecret') || ''
: '',
riskSignals: {
ipAddress: '',
sessionId: '',
Expand All @@ -72,6 +82,8 @@ export const useMainStore = defineStore('main', {
getEntitySecret: (state): string => state.entitySecret,
getWalletId: (state): string => state.walletId,
getFunderWalletId: (state): string => state.funderWalletId,
getFunderWalletApiKey: (state): string => state.funderWalletApiKey,
getFunderEntitySecret: (state): string => state.funderEntitySecret,
getRiskSignals: (state): RiskSignals => state.riskSignals,
getRequestPayload: (state): any => state.apiRequest.payload,
getRequestResponse: (state): any => state.apiRequest.response,
Expand Down Expand Up @@ -116,6 +128,20 @@ export const useMainStore = defineStore('main', {
}
},

setFunderWalletApiKey(funderWalletApiKey: string) {
this.funderWalletApiKey = funderWalletApiKey
if (typeof window !== 'undefined') {
localStorage.setItem('funderWalletApiKey', funderWalletApiKey)
}
},

setFunderEntitySecret(funderEntitySecret: string) {
this.funderEntitySecret = funderEntitySecret
if (typeof window !== 'undefined') {
localStorage.setItem('funderEntitySecret', funderEntitySecret)
}
},

setRiskSignals(riskSignals: RiskSignals) {
this.riskSignals = riskSignals
},
Expand Down
Loading