package keeper import ( "bytes" "encoding/hex" "fmt" "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/bep3/types" ) // CreateAtomicSwap creates a new AtomicSwap func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, timestamp int64, heightSpan int64, sender sdk.AccAddress, recipient sdk.AccAddress, senderOtherChain, recipientOtherChain string, amount sdk.Coins, expectedIncome string, crossChain bool) sdk.Error { // Confirm that this is not a duplicate swap swapID := types.CalculateSwapID(randomNumberHash, sender, senderOtherChain) _, found := k.GetAtomicSwap(ctx, swapID) if found { return types.ErrAtomicSwapAlreadyExists(k.codespace, swapID) } // The heightSpan period should be more than 10 minutes and less than one week // Assume average block time interval is 10 second. 10 mins = 60 blocks, 1 week = 60480 blocks if heightSpan < k.GetMinBlockLock(ctx) || heightSpan > k.GetMaxBlockLock(ctx) { return types.ErrInvalidHeightSpan(k.codespace, heightSpan, k.GetMinBlockLock(ctx), k.GetMaxBlockLock(ctx)) } // Unix timestamp must be in range [-15 mins, 30 mins] of the current time pastTimestampLimit := ctx.BlockTime().Add(time.Duration(-15) * time.Minute).Unix() futureTimestampLimit := ctx.BlockTime().Add(time.Duration(30) * time.Minute).Unix() if timestamp < pastTimestampLimit || timestamp >= futureTimestampLimit { return types.ErrInvalidTimestamp(k.codespace) } // Sanity check on recipient address if recipient.Empty() { return sdk.ErrInvalidAddress("invalid (empty) recipient address") } if len(amount) != 1 { return sdk.ErrInternal("amount must contain exactly one coin") } err := k.ValidateLiveAsset(ctx, amount[0]) if err != nil { return err } var direction types.SwapDirection deputy := k.GetBnbDeputyAddress(ctx) if sender.Equals(deputy) { direction = types.Incoming } else { direction = types.Outgoing } switch direction { case types.Incoming: err := k.IncrementIncomingAssetSupply(ctx, amount[0]) if err != nil { return err } case types.Outgoing: err := k.IncrementOutgoingAssetSupply(ctx, amount[0]) if err != nil { return err } default: return sdk.ErrInternal("invalid swap direction") } // Transfer coins to module err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount) if err != nil { return err } // Store the details of the swap atomicSwap := types.NewAtomicSwap(amount, randomNumberHash, ctx.BlockHeight()+heightSpan, timestamp, sender, recipient, senderOtherChain, recipientOtherChain, 0, types.Open, crossChain, direction) // Emit 'create_atomic_swap' event ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeCreateAtomicSwap, sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", atomicSwap.Sender)), sdk.NewAttribute(types.AttributeKeyRecipient, fmt.Sprintf("%s", atomicSwap.Recipient)), sdk.NewAttribute(types.AttributeKeyAtomicSwapID, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.GetSwapID()))), sdk.NewAttribute(types.AttributeKeyRandomNumberHash, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.RandomNumberHash))), sdk.NewAttribute(types.AttributeKeyTimestamp, fmt.Sprintf("%d", atomicSwap.Timestamp)), sdk.NewAttribute(types.AttributeKeySenderOtherChain, fmt.Sprintf("%s", atomicSwap.SenderOtherChain)), sdk.NewAttribute(types.AttributeKeyExpireHeight, fmt.Sprintf("%d", atomicSwap.ExpireHeight)), sdk.NewAttribute(types.AttributeKeyAmount, fmt.Sprintf("%s", atomicSwap.Amount[0].String())), sdk.NewAttribute(types.AttributeKeyExpectedIncome, fmt.Sprintf("%s", expectedIncome)), sdk.NewAttribute(types.AttributeKeyDirection, fmt.Sprintf("%s", atomicSwap.Direction.String())), ), ) k.SetAtomicSwap(ctx, atomicSwap) k.InsertIntoByBlockIndex(ctx, atomicSwap) return nil } // ClaimAtomicSwap validates a claim attempt, and if successful, sends the escrowed amount and closes the AtomicSwap func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte, randomNumber []byte) sdk.Error { atomicSwap, found := k.GetAtomicSwap(ctx, swapID) if !found { return types.ErrAtomicSwapNotFound(k.codespace, swapID) } // Only open atomic swaps can be claimed if atomicSwap.Status != types.Open { return types.ErrSwapNotClaimable(k.codespace) } // Calculate hashed secret using submitted number hashedSubmittedNumber := types.CalculateRandomHash(randomNumber, atomicSwap.Timestamp) hashedSecret := types.CalculateSwapID(hashedSubmittedNumber, atomicSwap.Sender, atomicSwap.SenderOtherChain) // Confirm that secret unlocks the atomic swap if !bytes.Equal(hashedSecret, atomicSwap.GetSwapID()) { return types.ErrInvalidClaimSecret(k.codespace, hashedSecret, atomicSwap.GetSwapID()) } switch atomicSwap.Direction { case types.Incoming: err := k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0]) if err != nil { return err } err = k.IncrementCurrentAssetSupply(ctx, atomicSwap.Amount[0]) if err != nil { return err } case types.Outgoing: err := k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0]) if err != nil { return err } err = k.DecrementCurrentAssetSupply(ctx, atomicSwap.Amount[0]) if err != nil { return err } default: return sdk.ErrInternal("invalid swap direction") } // Send intended recipient coins err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Recipient, atomicSwap.Amount) if err != nil { return err } // Emit 'claim_atomic_swap' event ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeClaimAtomicSwap, sdk.NewAttribute(types.AttributeKeyClaimSender, fmt.Sprintf("%s", from)), sdk.NewAttribute(types.AttributeKeyRecipient, fmt.Sprintf("%s", atomicSwap.Recipient)), sdk.NewAttribute(types.AttributeKeyAtomicSwapID, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.GetSwapID()))), sdk.NewAttribute(types.AttributeKeyRandomNumberHash, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.RandomNumberHash))), sdk.NewAttribute(types.AttributeKeyRandomNumber, fmt.Sprintf("%s", hex.EncodeToString(randomNumber))), ), ) // Complete swap atomicSwap.Status = types.Completed atomicSwap.ClosedBlock = ctx.BlockHeight() k.SetAtomicSwap(ctx, atomicSwap) // Remove from byBlock index and transition to longterm storage k.RemoveFromByBlockIndex(ctx, atomicSwap) k.InsertIntoLongtermStorage(ctx, atomicSwap) return nil } // RefundAtomicSwap refunds an AtomicSwap, sending assets to the original sender and closing the AtomicSwap func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte) sdk.Error { atomicSwap, found := k.GetAtomicSwap(ctx, swapID) if !found { return types.ErrAtomicSwapNotFound(k.codespace, swapID) } // Only expired swaps may be refunded if atomicSwap.Status != types.Expired { return types.ErrSwapNotRefundable(k.codespace) } switch atomicSwap.Direction { case types.Incoming: err := k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0]) if err != nil { return err } case types.Outgoing: err := k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0]) if err != nil { return err } default: return sdk.ErrInternal("invalid swap direction") } // Refund coins to original swap sender err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Sender, atomicSwap.Amount) if err != nil { return err } // Emit 'refund_atomic_swap' event ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeRefundAtomicSwap, sdk.NewAttribute(types.AttributeKeyRefundSender, fmt.Sprintf("%s", from)), sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", atomicSwap.Sender)), sdk.NewAttribute(types.AttributeKeyAtomicSwapID, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.GetSwapID()))), sdk.NewAttribute(types.AttributeKeyRandomNumberHash, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.RandomNumberHash))), ), ) // Complete swap atomicSwap.Status = types.Completed atomicSwap.ClosedBlock = ctx.BlockHeight() k.SetAtomicSwap(ctx, atomicSwap) // Transition to longterm storage k.InsertIntoLongtermStorage(ctx, atomicSwap) return nil } // UpdateExpiredAtomicSwaps finds all AtomicSwaps that are past (or at) their ending times and expires them. func (k Keeper) UpdateExpiredAtomicSwaps(ctx sdk.Context) sdk.Error { var expiredSwaps [][]byte k.IterateAtomicSwapsByBlock(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool { expiredSwaps = append(expiredSwaps, id) return false }) // Expire incomplete swaps (claimed swaps have already been removed from byBlock index) for _, id := range expiredSwaps { swap, _ := k.GetAtomicSwap(ctx, id) swap.Status = types.Expired k.SetAtomicSwap(ctx, swap) k.RemoveFromByBlockIndex(ctx, swap) } return nil } // DeleteClosedAtomicSwapsFromLongtermStorage removes swaps one week after completion func (k Keeper) DeleteClosedAtomicSwapsFromLongtermStorage(ctx sdk.Context) sdk.Error { var swapsToDelete [][]byte k.IterateAtomicSwapsLongtermStorage(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool { swapsToDelete = append(swapsToDelete, id) return false }) // Delete closed atomic swaps for _, id := range swapsToDelete { swap, _ := k.GetAtomicSwap(ctx, id) k.RemoveAtomicSwap(ctx, swap.GetSwapID()) k.RemoveFromLongtermStorage(ctx, swap) } return nil }