Skip to content

Commit 488ed46

Browse files
committed
Implement swap cancel flow from funded state
1 parent e133cf8 commit 488ed46

File tree

4 files changed

+325
-36
lines changed

4 files changed

+325
-36
lines changed

pkg/code/async/swap/config.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ const (
1313
BatchSizeConfigEnvName = envConfigPrefix + "WORKER_BATCH_SIZE"
1414
defaultFulfillmentBatchSize = 100
1515

16-
ClientFundingTimeoutConfigEnvName = envConfigPrefix + "CLIENT_FUNDING_TIMEOUT"
17-
defaultClientFundingTimeout = 3 * time.Minute
16+
ClientTimeoutToFundConfigEnvName = envConfigPrefix + "CLIENT_TIMEOUT_TO_FUND"
17+
defaultClientTimeoutToFund = 3 * time.Minute
18+
19+
ClientTimeoutToSwapConfigEnvName = envConfigPrefix + "CLIENT_TIMEOUT_TO_SWAP"
20+
defaultClientTimeoutToSwap = 5 * time.Minute
1821
)
1922

2023
type conf struct {
21-
batchSize config.Uint64
22-
clientFundingTimeout config.Duration
24+
batchSize config.Uint64
25+
clientTimeoutToFund config.Duration
26+
clientTimeoutToSwap config.Duration
2327
}
2428

2529
// ConfigProvider defines how config values are pulled
@@ -29,8 +33,9 @@ type ConfigProvider func() *conf
2933
func WithEnvConfigs() ConfigProvider {
3034
return func() *conf {
3135
return &conf{
32-
batchSize: env.NewUint64Config(BatchSizeConfigEnvName, defaultFulfillmentBatchSize),
33-
clientFundingTimeout: env.NewDurationConfig(ClientFundingTimeoutConfigEnvName, defaultClientFundingTimeout),
36+
batchSize: env.NewUint64Config(BatchSizeConfigEnvName, defaultFulfillmentBatchSize),
37+
clientTimeoutToFund: env.NewDurationConfig(ClientTimeoutToFundConfigEnvName, defaultClientTimeoutToFund),
38+
clientTimeoutToSwap: env.NewDurationConfig(ClientTimeoutToSwapConfigEnvName, defaultClientTimeoutToSwap),
3439
}
3540
}
3641
}

pkg/code/async/swap/service.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,39 @@ import (
66

77
"github.com/sirupsen/logrus"
88

9+
indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1"
10+
911
"github.com/code-payments/code-server/pkg/code/async"
1012
code_data "github.com/code-payments/code-server/pkg/code/data"
1113
"github.com/code-payments/code-server/pkg/code/data/swap"
1214
)
1315

1416
type service struct {
15-
log *logrus.Entry
16-
conf *conf
17-
data code_data.Provider
18-
integration Integration
17+
log *logrus.Entry
18+
conf *conf
19+
data code_data.Provider
20+
vmIndexerClient indexerpb.IndexerClient
21+
integration Integration
1922
}
2023

21-
func New(data code_data.Provider, integration Integration, configProvider ConfigProvider) async.Service {
24+
func New(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, integration Integration, configProvider ConfigProvider) async.Service {
2225
return &service{
23-
log: logrus.StandardLogger().WithField("service", "swap"),
24-
conf: configProvider(),
25-
data: data,
26-
integration: integration,
26+
log: logrus.StandardLogger().WithField("service", "swap"),
27+
conf: configProvider(),
28+
data: data,
29+
vmIndexerClient: vmIndexerClient,
30+
integration: integration,
2731
}
2832

2933
}
3034

3135
func (p *service) Start(ctx context.Context, interval time.Duration) error {
32-
3336
for _, state := range []swap.State{
3437
swap.StateCreated,
3538
swap.StateFunding,
39+
swap.StateFunded,
3640
swap.StateSubmitting,
41+
swap.StateCancelling,
3742
} {
3843
go func(state swap.State) {
3944

pkg/code/async/swap/util.go

Lines changed: 229 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import (
1919
"github.com/code-payments/code-server/pkg/code/data/swap"
2020
"github.com/code-payments/code-server/pkg/code/data/transaction"
2121
transaction_util "github.com/code-payments/code-server/pkg/code/transaction"
22+
"github.com/code-payments/code-server/pkg/solana"
23+
compute_budget "github.com/code-payments/code-server/pkg/solana/computebudget"
24+
"github.com/code-payments/code-server/pkg/solana/cvm"
25+
"github.com/code-payments/code-server/pkg/solana/memo"
26+
"github.com/code-payments/code-server/pkg/solana/system"
2227
)
2328

2429
func (p *service) validateSwapState(record *swap.Record, states ...swap.State) error {
@@ -74,25 +79,77 @@ func (p *service) markSwapFailed(ctx context.Context, record *swap.Record) error
7479
})
7580
}
7681

77-
func (p *service) markSwapCancelled(ctx context.Context, record *swap.Record) error {
82+
func (p *service) markSwapCancelling(ctx context.Context, record *swap.Record, txn *solana.Transaction) error {
7883
return p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error {
79-
err := p.validateSwapState(record, swap.StateCreated)
84+
err := p.validateSwapState(record, swap.StateFunded)
8085
if err != nil {
8186
return err
8287
}
8388

84-
err = p.markNonceAvailableDueToCancelledSwap(ctx, record)
89+
txnSignature := base58.Encode(txn.Signature())
90+
91+
err = transaction_util.UpdateNonceSignature(ctx, p.data, record.Nonce, record.ProofSignature, txnSignature)
8592
if err != nil {
8693
return err
8794
}
8895

96+
record.TransactionSignature = &txnSignature
97+
record.TransactionBlob = txn.Marshal()
98+
record.State = swap.StateCancelling
99+
return p.data.SaveSwap(ctx, record)
100+
})
101+
}
102+
103+
func (p *service) markSwapCancelled(ctx context.Context, record *swap.Record) error {
104+
return p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error {
105+
err := p.validateSwapState(record, swap.StateCreated, swap.StateCancelling)
106+
if err != nil {
107+
return err
108+
}
109+
110+
switch record.State {
111+
case swap.StateCreated:
112+
err = p.markNonceAvailableDueToCancelledSwap(ctx, record)
113+
if err != nil {
114+
return err
115+
}
116+
case swap.StateCancelling:
117+
err = p.markNonceReleasedDueToSubmittedTransaction(ctx, record)
118+
if err != nil {
119+
return err
120+
}
121+
}
122+
123+
record.TransactionBlob = nil
89124
record.State = swap.StateCancelled
90125
return p.data.SaveSwap(ctx, record)
91126
})
92127
}
93128

94-
// todo: commonalities between this and geyser external deposit logic
95-
func (p *service) updateBalances(ctx context.Context, record *swap.Record) error {
129+
func (p *service) submitTransaction(ctx context.Context, record *swap.Record) error {
130+
err := p.validateSwapState(record, swap.StateSubmitting, swap.StateCancelling)
131+
if err != nil {
132+
return err
133+
}
134+
135+
var txn solana.Transaction
136+
err = txn.Unmarshal(record.TransactionBlob)
137+
if err != nil {
138+
return errors.Wrap(err, "error unmarshalling transaction")
139+
}
140+
141+
if base58.Encode(txn.Signature()) != *record.TransactionSignature {
142+
return errors.New("unexpected transaction signature")
143+
}
144+
145+
_, err = p.data.SubmitBlockchainTransaction(ctx, &txn)
146+
if err != nil {
147+
return errors.Wrap(err, "error submitting transaction")
148+
}
149+
return nil
150+
}
151+
152+
func (p *service) updateBalancesForFinalizedSwap(ctx context.Context, record *swap.Record) error {
96153
owner, err := common.NewAccountFromPublicKeyString(record.Owner)
97154
if err != nil {
98155
return err
@@ -171,6 +228,85 @@ func (p *service) updateBalances(ctx context.Context, record *swap.Record) error
171228
})
172229
}
173230

231+
func (p *service) updateBalancesForCancelledSwap(ctx context.Context, record *swap.Record) error {
232+
owner, err := common.NewAccountFromPublicKeyString(record.Owner)
233+
if err != nil {
234+
return err
235+
}
236+
237+
fromMint, err := common.NewAccountFromPublicKeyString(record.FromMint)
238+
if err != nil {
239+
return err
240+
}
241+
242+
soureVmConfig, err := common.GetVmConfigForMint(ctx, p.data, fromMint)
243+
if err != nil {
244+
return err
245+
}
246+
247+
ownerSourceTimelockVault, err := owner.ToTimelockVault(soureVmConfig)
248+
if err != nil {
249+
return err
250+
}
251+
252+
tokenBalances, err := p.data.GetBlockchainTransactionTokenBalances(ctx, *record.TransactionSignature)
253+
if err != nil {
254+
return err
255+
}
256+
257+
deltaQuarksIntoOmnibus, err := transaction_util.GetDeltaQuarksFromTokenBalances(soureVmConfig.Omnibus, tokenBalances)
258+
if err != nil {
259+
return err
260+
}
261+
if deltaQuarksIntoOmnibus <= 0 {
262+
return errors.New("delta quarks into destination vm omnibus is not positive")
263+
}
264+
265+
usdMarketValue, _, err := currency_util.CalculateUsdMarketValue(ctx, p.data, fromMint, uint64(deltaQuarksIntoOmnibus), time.Now())
266+
if err != nil {
267+
return err
268+
}
269+
270+
return p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error {
271+
// For transaction history
272+
intentRecord := &intent.Record{
273+
IntentId: getSwapDepositIntentID(*record.TransactionSignature, ownerSourceTimelockVault),
274+
IntentType: intent.ExternalDeposit,
275+
276+
MintAccount: fromMint.PublicKey().ToBase58(),
277+
278+
InitiatorOwnerAccount: owner.PublicKey().ToBase58(),
279+
280+
ExternalDepositMetadata: &intent.ExternalDepositMetadata{
281+
DestinationTokenAccount: ownerSourceTimelockVault.PublicKey().ToBase58(),
282+
Quantity: uint64(deltaQuarksIntoOmnibus),
283+
UsdMarketValue: usdMarketValue,
284+
},
285+
286+
State: intent.StateConfirmed,
287+
CreatedAt: time.Now(),
288+
}
289+
err = p.data.SaveIntent(ctx, intentRecord)
290+
if err != nil {
291+
return err
292+
}
293+
294+
// For tracking in cached balances
295+
externalDepositRecord := &deposit.Record{
296+
Signature: *record.TransactionSignature,
297+
Destination: ownerSourceTimelockVault.PublicKey().ToBase58(),
298+
Amount: uint64(deltaQuarksIntoOmnibus),
299+
UsdMarketValue: usdMarketValue,
300+
301+
Slot: tokenBalances.Slot,
302+
ConfirmationState: transaction.ConfirmationFinalized,
303+
304+
CreatedAt: time.Now(),
305+
}
306+
return p.data.SaveExternalDeposit(ctx, externalDepositRecord)
307+
})
308+
}
309+
174310
func (p *service) notifySwapFinalized(ctx context.Context, swapRecord *swap.Record) error {
175311
owner, err := common.NewAccountFromPublicKeyString(swapRecord.Owner)
176312
if err != nil {
@@ -199,8 +335,95 @@ func (p *service) notifySwapFinalized(ctx context.Context, swapRecord *swap.Reco
199335
return p.integration.OnSwapFinalized(ctx, owner, toMint, currencyName, fundingIntentRecord.SendPublicPaymentMetadata.ExchangeCurrency, fundingIntentRecord.SendPublicPaymentMetadata.NativeAmount)
200336
}
201337

338+
// todo: put this in transaction utility package
339+
func (p *service) getCancellationTransaction(ctx context.Context, record *swap.Record) (*solana.Transaction, error) {
340+
owner, err := common.NewAccountFromPublicKeyString(record.Owner)
341+
if err != nil {
342+
return nil, err
343+
}
344+
345+
fromMint, err := common.NewAccountFromPublicKeyString(record.FromMint)
346+
if err != nil {
347+
return nil, err
348+
}
349+
350+
nonce, err := common.NewAccountFromPublicKeyString(record.Nonce)
351+
if err != nil {
352+
return nil, err
353+
}
354+
355+
decodedBlockhash, err := base58.Decode(record.Blockhash)
356+
if err != nil {
357+
return nil, err
358+
}
359+
360+
sourceVmConfig, err := common.GetVmConfigForMint(ctx, p.data, fromMint)
361+
if err != nil {
362+
return nil, err
363+
}
364+
365+
sourceOwnerVmSwapPdaAccounts, err := owner.GetVmSwapAccounts(sourceVmConfig)
366+
if err != nil {
367+
return nil, err
368+
}
369+
370+
memoryAccount, memoryIndex, err := common.GetVirtualTimelockAccountLocationInMemory(ctx, p.vmIndexerClient, sourceVmConfig.Vm, owner)
371+
if err != nil {
372+
return nil, err
373+
}
374+
375+
txn := solana.NewLegacyTransaction(
376+
common.GetSubsidizer().PublicKey().ToBytes(),
377+
compute_budget.SetComputeUnitLimit(200_000), // todo: optimize this
378+
compute_budget.SetComputeUnitPrice(1_000),
379+
memo.Instruction("cancel_swap_v0"),
380+
system.AdvanceNonce(nonce.PublicKey().ToBytes(), common.GetSubsidizer().PublicKey().ToBytes()),
381+
cvm.NewCancelSwapInstruction(
382+
&cvm.CancelSwapInstructionAccounts{
383+
VmAuthority: sourceVmConfig.Authority.PublicKey().ToBytes(),
384+
Vm: sourceVmConfig.Vm.PublicKey().ToBytes(),
385+
VmMemory: memoryAccount.PublicKey().ToBytes(),
386+
Swapper: owner.PublicKey().ToBytes(),
387+
SwapPda: sourceOwnerVmSwapPdaAccounts.Pda.PublicKey().ToBytes(),
388+
SwapAta: sourceOwnerVmSwapPdaAccounts.Ata.PublicKey().ToBytes(),
389+
VmOmnibus: sourceVmConfig.Omnibus.PublicKey().ToBytes(),
390+
},
391+
&cvm.CancelSwapInstructionArgs{
392+
AccountIndex: memoryIndex,
393+
Amount: record.Amount,
394+
Bump: sourceOwnerVmSwapPdaAccounts.PdaBump,
395+
},
396+
),
397+
cvm.NewCloseSwapAccountIfEmptyInstruction(
398+
&cvm.CloseSwapAccountIfEmptyInstructionAccounts{
399+
VmAuthority: sourceVmConfig.Authority.PublicKey().ToBytes(),
400+
Vm: sourceVmConfig.Vm.PublicKey().ToBytes(),
401+
Swapper: owner.PublicKey().ToBytes(),
402+
SwapPda: sourceOwnerVmSwapPdaAccounts.Pda.PublicKey().ToBytes(),
403+
SwapAta: sourceOwnerVmSwapPdaAccounts.Ata.PublicKey().ToBytes(),
404+
Destination: common.GetSubsidizer().PublicKey().ToBytes(),
405+
},
406+
&cvm.CloseSwapAccountIfEmptyInstructionArgs{
407+
Bump: sourceOwnerVmSwapPdaAccounts.PdaBump,
408+
},
409+
),
410+
)
411+
412+
txn.SetBlockhash(solana.Blockhash(decodedBlockhash))
413+
414+
err = txn.Sign(
415+
common.GetSubsidizer().PrivateKey().ToBytes(),
416+
sourceVmConfig.Authority.PrivateKey().ToBytes(),
417+
)
418+
if err != nil {
419+
return nil, err
420+
}
421+
422+
return &txn, nil
423+
}
424+
202425
func (p *service) markNonceReleasedDueToSubmittedTransaction(ctx context.Context, record *swap.Record) error {
203-
err := p.validateSwapState(record, swap.StateSubmitting)
426+
err := p.validateSwapState(record, swap.StateSubmitting, swap.StateCancelling)
204427
if err != nil {
205428
return err
206429
}

0 commit comments

Comments
 (0)