From 001b846f42bb6030b53319ec736d2ed13272d1da Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 26 Jun 2024 15:04:55 -0400 Subject: [PATCH 01/79] Add support for CVM memory (#141) --- pkg/solana/cvm/accounts_memory_account.go | 61 +++++++ pkg/solana/cvm/address.go | 150 ++++++++++++++++++ .../cvm/instructions_system_nonce_init.go | 3 + .../cvm/instructions_system_timelock_init.go | 3 + pkg/solana/cvm/instructions_vm_init.go | 85 ++++++++++ pkg/solana/cvm/instructions_vm_memory_init.go | 75 +++++++++ .../cvm/instructions_vm_memory_resize.go | 74 +++++++++ pkg/solana/cvm/legacy.go | 20 +++ pkg/solana/cvm/program.go | 43 +++++ pkg/solana/cvm/types_account_buffer.go | 86 ++++++++++ pkg/solana/cvm/types_account_index.go | 47 ++++++ pkg/solana/cvm/types_hash.go | 18 +++ pkg/solana/cvm/types_page.go | 47 ++++++ pkg/solana/cvm/types_sector.go | 66 ++++++++ pkg/solana/cvm/types_virtual_account_type.go | 9 ++ pkg/solana/cvm/utils.go | 92 +++++++++++ .../cvm/virtual_accounts_durable_nonce.go | 49 ++++++ .../cvm/virtual_accounts_relay_account.go | 57 +++++++ .../cvm/virtual_accounts_timelock_account.go | 71 +++++++++ 19 files changed, 1056 insertions(+) create mode 100644 pkg/solana/cvm/accounts_memory_account.go create mode 100644 pkg/solana/cvm/address.go create mode 100644 pkg/solana/cvm/instructions_system_nonce_init.go create mode 100644 pkg/solana/cvm/instructions_system_timelock_init.go create mode 100644 pkg/solana/cvm/instructions_vm_init.go create mode 100644 pkg/solana/cvm/instructions_vm_memory_init.go create mode 100644 pkg/solana/cvm/instructions_vm_memory_resize.go create mode 100644 pkg/solana/cvm/legacy.go create mode 100644 pkg/solana/cvm/program.go create mode 100644 pkg/solana/cvm/types_account_buffer.go create mode 100644 pkg/solana/cvm/types_account_index.go create mode 100644 pkg/solana/cvm/types_hash.go create mode 100644 pkg/solana/cvm/types_page.go create mode 100644 pkg/solana/cvm/types_sector.go create mode 100644 pkg/solana/cvm/types_virtual_account_type.go create mode 100644 pkg/solana/cvm/utils.go create mode 100644 pkg/solana/cvm/virtual_accounts_durable_nonce.go create mode 100644 pkg/solana/cvm/virtual_accounts_relay_account.go create mode 100644 pkg/solana/cvm/virtual_accounts_timelock_account.go diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go new file mode 100644 index 00000000..b181c121 --- /dev/null +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -0,0 +1,61 @@ +package cvm + +import ( + "bytes" + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const ( + MaxMemoryAccountNameLength = 32 +) + +type MemoryAccountWithData struct { + Vm ed25519.PublicKey + Bump uint8 + Name string + Data AccountBuffer +} + +const MemoryAccountWithDataSize = (8 + // discriminator + 32 + // vm + 1 + // bump + MaxMemoryAccountNameLength + // name + 1 + // padding + AccountBufferSize) // data + +var MemoryAccountDiscriminator = []byte{0x89, 0x7a, 0xdc, 0x6e, 0xdd, 0xca, 0x3e, 0x7f} + +func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { + if len(data) < MemoryAccountWithDataSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, MemoryAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.Vm, &offset) + getUint8(data, &obj.Bump, &offset) + getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) + offset += 1 // padding + getAccountBuffer(data, &obj.Data, &offset) + + return nil +} + +func (obj *MemoryAccountWithData) String() string { + return fmt.Sprintf( + "MemoryAccountWithData{vm=%s,bump=%d,name=%s,data=%s}", + base58.Encode(obj.Vm), + obj.Bump, + obj.Name, + obj.Data.String(), + ) +} diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go new file mode 100644 index 00000000..744e5264 --- /dev/null +++ b/pkg/solana/cvm/address.go @@ -0,0 +1,150 @@ +package cvm + +import ( + "crypto/ed25519" + "crypto/sha256" + + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + TimelockDataVersion1 = 3 +) + +var ( + CodeVmPrefix = []byte("code-vm") + TimelockStateAccountPrefix = []byte("timelock_state") + TimelockVaultAccountPrefix = []byte("timelock_state") + VmMemoryAccountPrefix = []byte("vm_memory_account") + VmOmnibusPrefix = []byte("vm_omnibus") + VmUnlockPdaAccountPrefix = []byte("vm_unlock_pda_account") + VmWithdrawReceiptAccountPrefix = []byte("vm_withdraw_receipt_account") +) + +type GetVmAddressArgs struct { + Mint ed25519.PublicKey + VmAuthority ed25519.PublicKey + LockDuration uint8 +} + +func GetVmAddress(args *GetVmAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + args.Mint, + args.VmAuthority, + []byte{args.LockDuration}, + ) +} + +type GetVmObnibusAddressArgs struct { + Mint ed25519.PublicKey + VmAuthority ed25519.PublicKey + LockDuration uint8 +} + +func GetVmObnibusAddress(args *GetVmObnibusAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmOmnibusPrefix, + args.Mint, + args.VmAuthority, + []byte{args.LockDuration}, + ) +} + +type GetMemoryAccountAddressArgs struct { + Name string + Vm ed25519.PublicKey +} + +func GetMemoryAccountAddress(args *GetMemoryAccountAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmMemoryAccountPrefix, + []byte(toFixedString(args.Name, MaxMemoryAccountNameLength)), + args.Vm, + ) +} + +type GetVirtualDurableNonceAddressArgs struct { + Seed ed25519.PublicKey + Value Hash +} + +func GetVirtualDurableNonceAddress(args *GetVirtualDurableNonceAddressArgs) ed25519.PublicKey { + var combined [64]byte + copy(combined[0:32], args.Seed) + copy(combined[32:64], args.Value[:]) + + h := sha256.New() + h.Write(combined[:]) + return h.Sum(nil) +} + +type GetVirtualTimelockAccountAddressArgs struct { + Mint ed25519.PublicKey + VmAuthority ed25519.PublicKey + Owner ed25519.PublicKey + LockDuration uint8 +} + +func GetVirtualTimelockAccountAddress(args *GetVirtualTimelockAccountAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + TIMELOCK_PROGRAM_ID, + TimelockStateAccountPrefix, + args.Mint, + args.VmAuthority, + args.Owner, + []byte{args.LockDuration}, + ) +} + +type GetVirtualTimelockVaultAddressArgs struct { + VirtualTimelock ed25519.PublicKey +} + +func GetVirtualTimelockVaultAddress(args *GetVirtualTimelockVaultAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + TIMELOCK_PROGRAM_ID, + TimelockVaultAccountPrefix, + args.VirtualTimelock, + []byte{byte(TimelockDataVersion1)}, + ) +} + +type GetUnlockStateAccountAddressArgs struct { + Owner ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + Vm ed25519.PublicKey +} + +func GetUnlockStateAccountAddress(args *GetUnlockStateAccountAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmUnlockPdaAccountPrefix, + args.Owner, + args.VirtualTimelock, + args.Vm, + ) +} + +type GetWithdrawReceiptAccountAddressArgs struct { + UnlockAccount ed25519.PublicKey + Nonce Hash + Vm ed25519.PublicKey +} + +func GetWithdrawReceiptAccountAddress(args *GetWithdrawReceiptAccountAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmWithdrawReceiptAccountPrefix, + args.UnlockAccount, + args.Nonce[:], + args.Vm, + ) +} diff --git a/pkg/solana/cvm/instructions_system_nonce_init.go b/pkg/solana/cvm/instructions_system_nonce_init.go new file mode 100644 index 00000000..93456883 --- /dev/null +++ b/pkg/solana/cvm/instructions_system_nonce_init.go @@ -0,0 +1,3 @@ +package cvm + +// todo: implement me diff --git a/pkg/solana/cvm/instructions_system_timelock_init.go b/pkg/solana/cvm/instructions_system_timelock_init.go new file mode 100644 index 00000000..93456883 --- /dev/null +++ b/pkg/solana/cvm/instructions_system_timelock_init.go @@ -0,0 +1,3 @@ +package cvm + +// todo: implement me diff --git a/pkg/solana/cvm/instructions_vm_init.go b/pkg/solana/cvm/instructions_vm_init.go new file mode 100644 index 00000000..9e7c5543 --- /dev/null +++ b/pkg/solana/cvm/instructions_vm_init.go @@ -0,0 +1,85 @@ +package cvm + +import ( + "crypto/ed25519" +) + +var VmInitInstructionDiscriminator = []byte{ + 0xd3, 0xd4, 0x60, 0x15, 0xc2, 0x62, 0xe1, 0xfc, +} + +const ( + VmInitInstructionArgsSize = 1 // lock_duration +) + +type VmInitInstructionArgs struct { + LockDuration uint8 +} + +type VmInitInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + Omnibus ed25519.PublicKey + Mint ed25519.PublicKey +} + +func NewVmInitInstruction( + accounts *VmInitInstructionAccounts, + args *VmInitInstructionArgs, +) Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(VmInitInstructionDiscriminator)+ + VmInitInstructionArgsSize) + + putDiscriminator(data, VmInitInstructionDiscriminator, &offset) + putUint8(data, args.LockDuration, &offset) + + return Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Omnibus, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Mint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/instructions_vm_memory_init.go b/pkg/solana/cvm/instructions_vm_memory_init.go new file mode 100644 index 00000000..72168079 --- /dev/null +++ b/pkg/solana/cvm/instructions_vm_memory_init.go @@ -0,0 +1,75 @@ +package cvm + +import ( + "crypto/ed25519" +) + +var VmMemoryInitInstructionDiscriminator = []byte{ + 0x05, 0xd3, 0xfb, 0x74, 0x39, 0xbc, 0xc1, 0xad, +} + +const ( + VmMemoryInitInstructionArgsSize = (4 + // len(name) + 32) // name +) + +type VmMemoryInitInstructionArgs struct { + Name string +} + +type VmMemoryInitInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey +} + +func NewVmMemoryInitInstruction( + accounts *VmMemoryInitInstructionAccounts, + args *VmMemoryInitInstructionArgs, +) Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(VmMemoryInitInstructionDiscriminator)+ + VmMemoryInitInstructionArgsSize) + + putDiscriminator(data, VmMemoryInitInstructionDiscriminator, &offset) + putString(data, args.Name, &offset) + + return Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmMemory, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/instructions_vm_memory_resize.go b/pkg/solana/cvm/instructions_vm_memory_resize.go new file mode 100644 index 00000000..a0e98280 --- /dev/null +++ b/pkg/solana/cvm/instructions_vm_memory_resize.go @@ -0,0 +1,74 @@ +package cvm + +import ( + "crypto/ed25519" +) + +var VmMemoryResizeInstructionDiscriminator = []byte{ + 0x6a, 0x40, 0x89, 0xc0, 0xdc, 0x48, 0xcd, 0x59, +} + +const ( + VmMemoryResizeInstructionArgsSize = 4 // len +) + +type VmMemoryResizeInstructionArgs struct { + Len uint32 +} + +type VmMemoryResizeInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey +} + +func NewVmMemoryResizeInstruction( + accounts *VmMemoryResizeInstructionAccounts, + args *VmMemoryResizeInstructionArgs, +) Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(VmMemoryResizeInstructionDiscriminator)+ + VmMemoryResizeInstructionArgsSize) + + putDiscriminator(data, VmMemoryResizeInstructionDiscriminator, &offset) + putUint32(data, args.Len, &offset) + + return Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmMemory, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/legacy.go b/pkg/solana/cvm/legacy.go new file mode 100644 index 00000000..d3c9848d --- /dev/null +++ b/pkg/solana/cvm/legacy.go @@ -0,0 +1,20 @@ +package cvm + +import "github.com/code-payments/code-server/pkg/solana" + +func (i Instruction) ToLegacyInstruction() solana.Instruction { + legacyAccountMeta := make([]solana.AccountMeta, len(i.Accounts)) + for i, accountMeta := range i.Accounts { + legacyAccountMeta[i] = solana.AccountMeta{ + PublicKey: accountMeta.PublicKey, + IsSigner: accountMeta.IsSigner, + IsWritable: accountMeta.IsWritable, + } + } + + return solana.Instruction{ + Program: PROGRAM_ID, + Accounts: legacyAccountMeta, + Data: i.Data, + } +} diff --git a/pkg/solana/cvm/program.go b/pkg/solana/cvm/program.go new file mode 100644 index 00000000..f9b3d47e --- /dev/null +++ b/pkg/solana/cvm/program.go @@ -0,0 +1,43 @@ +package cvm + +import ( + "crypto/ed25519" + "errors" +) + +var ( + ErrInvalidProgram = errors.New("invalid program id") + ErrInvalidAccountData = errors.New("unexpected account data") + ErrInvalidVirtualAccountData = errors.New("unexpected virtual account data") + ErrInvalidVirtualAccountType = errors.New("unexpected virtual account type") + ErrInvalidInstructionData = errors.New("unexpected instruction data") +) + +var ( + // todo: setup real program address + PROGRAM_ADDRESS = mustBase58Decode("HzNbpGCu2S8fdbkVRJYnenj9BrSwW29tGuCQrgXdnmuc") + PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) +) + +var ( + SYSTEM_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("11111111111111111111111111111111")) + SPL_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) + TIMELOCK_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("time2Z2SCnn3qYg3ULKVtdkh8YmZ5jFdKicnA1W2YnJ")) + + SYSVAR_RENT_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarRent111111111111111111111111111111111")) +) + +// AccountMeta represents the account information required +// for building transactions. +type AccountMeta struct { + PublicKey ed25519.PublicKey + IsWritable bool + IsSigner bool +} + +// Instruction represents a transaction instruction. +type Instruction struct { + Program ed25519.PublicKey + Accounts []AccountMeta + Data []byte +} diff --git a/pkg/solana/cvm/types_account_buffer.go b/pkg/solana/cvm/types_account_buffer.go new file mode 100644 index 00000000..74752d97 --- /dev/null +++ b/pkg/solana/cvm/types_account_buffer.go @@ -0,0 +1,86 @@ +package cvm + +import ( + "fmt" + "strings" +) + +const ( + NumAccounts = 100 + NumSectors = 2 +) + +const AccountBufferSize = (NumAccounts*AccountIndexSize + // accounts + NumSectors*SectorSize) // sectors + +type AccountBuffer struct { + Accounts []AccountIndex + Sectors []Sector +} + +func (obj *AccountBuffer) ReadAccount(index int) ([]byte, bool) { + if index >= len(obj.Accounts) { + return nil, false + } + + account := obj.Accounts[index] + if !account.IsAllocated() { + return nil, false + } + + pages := obj.Sectors[account.Sector].GetLinkedPages(account.Page) + + var data []byte + for _, page := range pages { + if !page.IsAllocated { + return nil, false + } + + data = append(data, page.Data...) + } + + return data[:account.Size], true +} + +func (obj *AccountBuffer) Unmarshal(data []byte) error { + if len(data) < AccountBufferSize { + return ErrInvalidAccountData + } + + var offset int + + obj.Accounts = make([]AccountIndex, NumAccounts) + obj.Sectors = make([]Sector, NumSectors) + + for i := 0; i < NumAccounts; i++ { + getAccountIndex(data, &obj.Accounts[i], &offset) + } + for i := 0; i < NumSectors; i++ { + getSector(data, &obj.Sectors[i], &offset) + } + + return nil +} + +func (obj *AccountBuffer) String() string { + accountStrings := make([]string, len(obj.Accounts)) + for i, account := range obj.Accounts { + accountStrings[i] = account.String() + } + + sectorStrings := make([]string, len(obj.Sectors)) + for i, page := range obj.Sectors { + sectorStrings[i] = page.String() + } + + return fmt.Sprintf( + "Sector{accounts=[%s],sectors=[%s]}", + strings.Join(accountStrings, ","), + strings.Join(sectorStrings, ","), + ) +} + +func getAccountBuffer(src []byte, dst *AccountBuffer, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += AccountBufferSize +} diff --git a/pkg/solana/cvm/types_account_index.go b/pkg/solana/cvm/types_account_index.go new file mode 100644 index 00000000..16e4a0d1 --- /dev/null +++ b/pkg/solana/cvm/types_account_index.go @@ -0,0 +1,47 @@ +package cvm + +import ( + "fmt" +) + +const AccountIndexSize = (2 + // size + 1 + // page + 1) // sector + +type AccountIndex struct { + Size uint16 + Page uint8 + Sector uint8 +} + +func (i *AccountIndex) IsAllocated() bool { + return i.Size != 0 +} + +func (obj *AccountIndex) Unmarshal(data []byte) error { + if len(data) < AccountIndexSize { + return ErrInvalidAccountData + } + + var offset int + + getUint16(data, &obj.Size, &offset) + getUint8(data, &obj.Page, &offset) + getUint8(data, &obj.Sector, &offset) + + return nil +} + +func (obj *AccountIndex) String() string { + return fmt.Sprintf( + "AccountIndex{size=%d,page=%d,sector=%d}", + obj.Size, + obj.Page, + obj.Sector, + ) +} + +func getAccountIndex(src []byte, dst *AccountIndex, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += AccountIndexSize +} diff --git a/pkg/solana/cvm/types_hash.go b/pkg/solana/cvm/types_hash.go new file mode 100644 index 00000000..3aa98d01 --- /dev/null +++ b/pkg/solana/cvm/types_hash.go @@ -0,0 +1,18 @@ +package cvm + +import ( + "encoding/hex" +) + +const HashSize = 32 + +type Hash [HashSize]byte + +func (h Hash) String() string { + return hex.EncodeToString(h[:]) +} + +func getHash(src []byte, dst *Hash, offset *int) { + copy(dst[:], src[*offset:]) + *offset += HashSize +} diff --git a/pkg/solana/cvm/types_page.go b/pkg/solana/cvm/types_page.go new file mode 100644 index 00000000..2e796d30 --- /dev/null +++ b/pkg/solana/cvm/types_page.go @@ -0,0 +1,47 @@ +package cvm + +import "fmt" + +const ( + PageDataLen = 77 +) + +const PageSize = (1 + // is_allocated + PageDataLen + // data + 1) // NextPage + +type Page struct { + IsAllocated bool + Data []byte + NextPage uint8 +} + +func (obj *Page) Unmarshal(data []byte) error { + if len(data) < PageSize { + return ErrInvalidAccountData + } + + var offset int + + obj.Data = make([]byte, PageDataLen) + + getBool(data, &obj.IsAllocated, &offset) + getData(data, obj.Data, PageDataLen, &offset) + getUint8(data, &obj.NextPage, &offset) + + return nil +} + +func (obj *Page) String() string { + return fmt.Sprintf( + "Page{is_allocated=%v,data=%x,next_page=%d}", + obj.IsAllocated, + obj.Data, + obj.NextPage, + ) +} + +func getPage(src []byte, dst *Page, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += PageSize +} diff --git a/pkg/solana/cvm/types_sector.go b/pkg/solana/cvm/types_sector.go new file mode 100644 index 00000000..f1f35e9d --- /dev/null +++ b/pkg/solana/cvm/types_sector.go @@ -0,0 +1,66 @@ +package cvm + +import ( + "fmt" + "strings" +) + +const ( + NumPages = 255 +) + +const SectorSize = (1 + // num_allocated + NumPages*PageSize) // pages + +type Sector struct { + NumAllocated uint8 + Pages []Page +} + +func (obj *Sector) GetLinkedPages(startIndex uint8) []Page { + var res []Page + current := startIndex + for { + res = append(res, obj.Pages[current]) + if obj.Pages[current].NextPage == 0 { + break + } + current = obj.Pages[current].NextPage + } + return res +} + +func (obj *Sector) Unmarshal(data []byte) error { + if len(data) < SectorSize { + return ErrInvalidAccountData + } + + var offset int + + obj.Pages = make([]Page, NumPages) + + getUint8(data, &obj.NumAllocated, &offset) + for i := 0; i < NumPages; i++ { + getPage(data, &obj.Pages[i], &offset) + } + + return nil +} + +func (obj *Sector) String() string { + pageStrings := make([]string, len(obj.Pages)) + for i, page := range obj.Pages { + pageStrings[i] = page.String() + } + + return fmt.Sprintf( + "Sector{num_allocated=%d,pages=[%s]}", + obj.NumAllocated, + strings.Join(pageStrings, ","), + ) +} + +func getSector(src []byte, dst *Sector, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += SectorSize +} diff --git a/pkg/solana/cvm/types_virtual_account_type.go b/pkg/solana/cvm/types_virtual_account_type.go new file mode 100644 index 00000000..81e10b98 --- /dev/null +++ b/pkg/solana/cvm/types_virtual_account_type.go @@ -0,0 +1,9 @@ +package cvm + +type VirtualAccountType uint8 + +const ( + VirtualAccountTypeDurableNonce VirtualAccountType = iota + VirtualAccountTypeTimelock + VirtualAccountTypeRelay +) diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go new file mode 100644 index 00000000..363df102 --- /dev/null +++ b/pkg/solana/cvm/utils.go @@ -0,0 +1,92 @@ +package cvm + +import ( + "crypto/ed25519" + "encoding/binary" + "strings" + + "github.com/mr-tron/base58" +) + +func putDiscriminator(dst []byte, v []byte, offset *int) { + copy(dst[*offset:], v) + *offset += 8 +} +func getDiscriminator(src []byte, dst *[]byte, offset *int) { + *dst = make([]byte, 8) + copy(*dst, src[*offset:]) + *offset += 8 +} + +func getKey(src []byte, dst *ed25519.PublicKey, offset *int) { + *dst = make([]byte, ed25519.PublicKeySize) + copy(*dst, src[*offset:]) + *offset += ed25519.PublicKeySize +} + +func getBool(src []byte, dst *bool, offset *int) { + if src[*offset] == 1 { + *dst = true + } else { + *dst = false + } + *offset += 1 +} + +func putString(dst []byte, src string, offset *int) { + putUint32(dst, uint32(len(src)), offset) + copy(dst[*offset:], src) + *offset += len(src) +} + +func getFixedString(data []byte, dst *string, length int, offset *int) { + *dst = string(data[*offset : *offset+length]) + *dst = removeFixedStringPadding(*dst) + *offset += length +} + +func getData(src []byte, dst []byte, length int, offset *int) { + copy(dst[:length], src[*offset:*offset+length]) + *offset += length +} + +func putUint8(dst []byte, v uint8, offset *int) { + dst[*offset] = v + *offset += 1 +} +func getUint8(src []byte, dst *uint8, offset *int) { + *dst = src[*offset] + *offset += 1 +} + +func getUint16(src []byte, dst *uint16, offset *int) { + *dst = binary.LittleEndian.Uint16(src[*offset:]) + *offset += 2 +} + +func putUint32(dst []byte, v uint32, offset *int) { + binary.LittleEndian.PutUint32(dst[*offset:], v) + *offset += 4 +} + +func getUint64(src []byte, dst *uint64, offset *int) { + *dst = binary.LittleEndian.Uint64(src[*offset:]) + *offset += 8 +} + +func toFixedString(value string, length int) string { + fixed := make([]byte, length) + copy(fixed, []byte(value)) + return string(fixed) +} +func removeFixedStringPadding(value string) string { + return strings.TrimRight(value, string([]byte{0})) +} + +func mustBase58Decode(value string) []byte { + decoded, err := base58.Decode(value) + if err != nil { + panic(err) + } + return decoded +} diff --git a/pkg/solana/cvm/virtual_accounts_durable_nonce.go b/pkg/solana/cvm/virtual_accounts_durable_nonce.go new file mode 100644 index 00000000..5d1eb454 --- /dev/null +++ b/pkg/solana/cvm/virtual_accounts_durable_nonce.go @@ -0,0 +1,49 @@ +package cvm + +import ( + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const VirtualDurableNonceSize = (32 + // address + 32) // hash + +type VirtualDurableNonce struct { + Address ed25519.PublicKey + Nonce Hash +} + +func (obj *VirtualDurableNonce) UnmarshalDirectly(data []byte) error { + if len(data) < VirtualDurableNonceSize { + return ErrInvalidVirtualAccountData + } + + var offset int + + getKey(data, &obj.Address, &offset) + getHash(data, &obj.Nonce, &offset) + + return nil +} + +func (obj *VirtualDurableNonce) UnmarshalFromMemory(data []byte) error { + if len(data) == 0 { + return ErrInvalidVirtualAccountData + } + + if data[0] != uint8(VirtualAccountTypeDurableNonce) { + return ErrInvalidVirtualAccountType + } + + return obj.UnmarshalDirectly(data[1:]) +} + +func (obj *VirtualDurableNonce) String() string { + return fmt.Sprintf( + "VirtualDurableNonce{address=%s,nonce=%s}", + base58.Encode(obj.Address), + obj.Nonce.String(), + ) +} diff --git a/pkg/solana/cvm/virtual_accounts_relay_account.go b/pkg/solana/cvm/virtual_accounts_relay_account.go new file mode 100644 index 00000000..1186603b --- /dev/null +++ b/pkg/solana/cvm/virtual_accounts_relay_account.go @@ -0,0 +1,57 @@ +package cvm + +import ( + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const VirtualRelayAccountSize = (32 + // address + 32 + // commitment + 32 + // recent_root + 32) // destination + +type VirtualRelayAccount struct { + Address ed25519.PublicKey + Commitment Hash + RecentRoot Hash + Destination ed25519.PublicKey +} + +func (obj *VirtualRelayAccount) UnmarshalDirectly(data []byte) error { + if len(data) < VirtualRelayAccountSize { + return ErrInvalidVirtualAccountData + } + + var offset int + + getKey(data, &obj.Address, &offset) + getHash(data, &obj.Commitment, &offset) + getHash(data, &obj.RecentRoot, &offset) + getKey(data, &obj.Destination, &offset) + + return nil +} + +func (obj *VirtualRelayAccount) UnmarshalFromMemory(data []byte) error { + if len(data) == 0 { + return ErrInvalidVirtualAccountData + } + + if data[0] != uint8(VirtualAccountTypeRelay) { + return ErrInvalidVirtualAccountType + } + + return obj.UnmarshalDirectly(data[1:]) +} + +func (obj *VirtualRelayAccount) String() string { + return fmt.Sprintf( + "VirtualRelayAccount{address=%s,commitment=%s,recent_root=%s,destination=%s}", + base58.Encode(obj.Address), + obj.Commitment.String(), + obj.RecentRoot.String(), + base58.Encode(obj.Destination), + ) +} diff --git a/pkg/solana/cvm/virtual_accounts_timelock_account.go b/pkg/solana/cvm/virtual_accounts_timelock_account.go new file mode 100644 index 00000000..282004da --- /dev/null +++ b/pkg/solana/cvm/virtual_accounts_timelock_account.go @@ -0,0 +1,71 @@ +package cvm + +import ( + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const VirtualTimelockAccountSize = (32 + // owner + 32 + // nonce + 1 + // token_bump + 1 + // unlock_bump + 1 + // withdraw_bump + 8 + // balance + 1) // bump + +type VirtualTimelockAccount struct { + Owner ed25519.PublicKey + Nonce Hash + + TokenBump uint8 + UnlockBump uint8 + WithdrawBump uint8 + + Balance uint64 + Bump uint8 +} + +func (obj *VirtualTimelockAccount) UnmarshalDirectly(data []byte) error { + if len(data) < VirtualTimelockAccountSize { + return ErrInvalidVirtualAccountData + } + + var offset int + + getKey(data, &obj.Owner, &offset) + getHash(data, &obj.Nonce, &offset) + getUint8(data, &obj.TokenBump, &offset) + getUint8(data, &obj.UnlockBump, &offset) + getUint8(data, &obj.WithdrawBump, &offset) + getUint64(data, &obj.Balance, &offset) + getUint8(data, &obj.Bump, &offset) + + return nil +} + +func (obj *VirtualTimelockAccount) UnmarshalFromMemory(data []byte) error { + if len(data) == 0 { + return ErrInvalidVirtualAccountData + } + + if data[0] != uint8(VirtualAccountTypeTimelock) { + return ErrInvalidVirtualAccountType + } + + return obj.UnmarshalDirectly(data[1:]) +} + +func (obj *VirtualTimelockAccount) String() string { + return fmt.Sprintf( + "VirtualTimelockAccount{owner=%s,nonce=%s,token_bump=%d,unlock_bump=%d,withdraw_bump=%d,balance=%d,bump=%d}", + base58.Encode(obj.Owner), + obj.Nonce.String(), + obj.TokenBump, + obj.UnlockBump, + obj.WithdrawBump, + obj.Balance, + obj.Bump, + ) +} From b4836ab0b6c83cfdd96073d8e7ee60e9219f0771 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 26 Jun 2024 16:38:38 -0400 Subject: [PATCH 02/79] Add VM instructions to initialize virtual accounts (#143) --- pkg/solana/cvm/address.go | 4 +- .../cvm/instructions_system_nonce_init.go | 67 +++++++++++++++- .../cvm/instructions_system_timelock_init.go | 76 ++++++++++++++++++- pkg/solana/cvm/instructions_vm_init.go | 4 +- pkg/solana/cvm/utils.go | 4 + 5 files changed, 149 insertions(+), 6 deletions(-) diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go index 744e5264..6e53827f 100644 --- a/pkg/solana/cvm/address.go +++ b/pkg/solana/cvm/address.go @@ -115,13 +115,13 @@ func GetVirtualTimelockVaultAddress(args *GetVirtualTimelockVaultAddressArgs) (e ) } -type GetUnlockStateAccountAddressArgs struct { +type GetVmUnlockStateAccountAddressArgs struct { Owner ed25519.PublicKey VirtualTimelock ed25519.PublicKey Vm ed25519.PublicKey } -func GetUnlockStateAccountAddress(args *GetUnlockStateAccountAddressArgs) (ed25519.PublicKey, uint8, error) { +func GetVmUnlockStateAccountAddress(args *GetVmUnlockStateAccountAddressArgs) (ed25519.PublicKey, uint8, error) { return solana.FindProgramAddressAndBump( PROGRAM_ID, CodeVmPrefix, diff --git a/pkg/solana/cvm/instructions_system_nonce_init.go b/pkg/solana/cvm/instructions_system_nonce_init.go index 93456883..1afebf56 100644 --- a/pkg/solana/cvm/instructions_system_nonce_init.go +++ b/pkg/solana/cvm/instructions_system_nonce_init.go @@ -1,3 +1,68 @@ package cvm -// todo: implement me +import "crypto/ed25519" + +var SystemNonceInitInstructionDiscriminator = []byte{ + 0x0d, 0xb2, 0x98, 0x60, 0xa7, 0x2c, 0x96, 0x30, +} + +const ( + SystemNonceInitInstructionArgsSize = 2 // account_index +) + +type SystemNonceInitInstructionArgs struct { + AccountIndex uint16 +} + +type SystemNonceInitInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey + VirtualAccountOwner ed25519.PublicKey +} + +func NewSystemNonceInitInstruction( + accounts *SystemNonceInitInstructionAccounts, + args *SystemNonceInitInstructionArgs, +) Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(SystemNonceInitInstructionDiscriminator)+ + SystemNonceInitInstructionArgsSize) + + putDiscriminator(data, SystemNonceInitInstructionDiscriminator, &offset) + putUint16(data, args.AccountIndex, &offset) + + return Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmMemory, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VirtualAccountOwner, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/instructions_system_timelock_init.go b/pkg/solana/cvm/instructions_system_timelock_init.go index 93456883..be6ae9ed 100644 --- a/pkg/solana/cvm/instructions_system_timelock_init.go +++ b/pkg/solana/cvm/instructions_system_timelock_init.go @@ -1,3 +1,77 @@ package cvm -// todo: implement me +import "crypto/ed25519" + +var SystemTimelockInitInstructionDiscriminator = []byte{ + 0x07, 0x0b, 0xf5, 0xc5, 0x68, 0xfe, 0xb7, 0xb6, +} + +const ( + SystemTimelockInitInstructionArgsSize = (2 + // account_index + 1 + // virtual_timelock_bump + 1 + // virtual_vault_bump + 1) // vm_unlock_pda_bump +) + +type SystemTimelockInitInstructionArgs struct { + AccountIndex uint16 + VirtualTimelockBump uint8 + VirtualVaultBump uint8 + VmUnlockPdaBump uint8 +} + +type SystemTimelockInitInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey + VirtualAccountOwner ed25519.PublicKey +} + +func NewSystemTimelockInitInstruction( + accounts *SystemTimelockInitInstructionAccounts, + args *SystemTimelockInitInstructionArgs, +) Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(SystemTimelockInitInstructionDiscriminator)+ + SystemTimelockInitInstructionArgsSize) + + putDiscriminator(data, SystemTimelockInitInstructionDiscriminator, &offset) + putUint16(data, args.AccountIndex, &offset) + putUint8(data, args.VirtualTimelockBump, &offset) + putUint8(data, args.VirtualVaultBump, &offset) + putUint8(data, args.VmUnlockPdaBump, &offset) + + return Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmMemory, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VirtualAccountOwner, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/instructions_vm_init.go b/pkg/solana/cvm/instructions_vm_init.go index 9e7c5543..34cd2d0c 100644 --- a/pkg/solana/cvm/instructions_vm_init.go +++ b/pkg/solana/cvm/instructions_vm_init.go @@ -19,7 +19,7 @@ type VmInitInstructionArgs struct { type VmInitInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey - Omnibus ed25519.PublicKey + VmOmnibus ed25519.PublicKey Mint ed25519.PublicKey } @@ -56,7 +56,7 @@ func NewVmInitInstruction( IsSigner: false, }, { - PublicKey: accounts.Omnibus, + PublicKey: accounts.VmOmnibus, IsWritable: true, IsSigner: false, }, diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go index 363df102..2fcf783a 100644 --- a/pkg/solana/cvm/utils.go +++ b/pkg/solana/cvm/utils.go @@ -59,6 +59,10 @@ func getUint8(src []byte, dst *uint8, offset *int) { *offset += 1 } +func putUint16(dst []byte, v uint16, offset *int) { + binary.LittleEndian.PutUint16(dst[*offset:], v) + *offset += 2 +} func getUint16(src []byte, dst *uint16, offset *int) { *dst = binary.LittleEndian.Uint16(src[*offset:]) *offset += 2 From fb0f4bb7d5266e061ffc925244cf8037c038e930 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 27 Jun 2024 13:16:56 -0400 Subject: [PATCH 03/79] Add VM instruction to deposit into Virtual Timelock Accounts (#144) --- .../cvm/instructions_timelock_deposit.go | 90 +++++++++++++++++++ pkg/solana/cvm/utils.go | 4 + 2 files changed, 94 insertions(+) create mode 100644 pkg/solana/cvm/instructions_timelock_deposit.go diff --git a/pkg/solana/cvm/instructions_timelock_deposit.go b/pkg/solana/cvm/instructions_timelock_deposit.go new file mode 100644 index 00000000..206e5f79 --- /dev/null +++ b/pkg/solana/cvm/instructions_timelock_deposit.go @@ -0,0 +1,90 @@ +package cvm + +import ( + "crypto/ed25519" +) + +var TimelockDepositInstructionDiscriminator = []byte{ + 0xe8, 0x19, 0x3f, 0x63, 0x5f, 0x15, 0xcc, 0xde, +} + +const ( + TimelockDepositInstructionArgsSize = (2 + // account_index + 8) // amount +) + +type TimelockDepositInstructionArgs struct { + AccountIndex uint16 + Amount uint64 +} + +type TimelockDepositInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey + VmOmnibus ed25519.PublicKey + DepositorAta ed25519.PublicKey + Depositor ed25519.PublicKey +} + +func NewTimelockDepositInstruction( + accounts *TimelockDepositInstructionAccounts, + args *TimelockDepositInstructionArgs, +) Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(TimelockDepositInstructionDiscriminator)+ + TimelockDepositInstructionArgsSize) + + putDiscriminator(data, TimelockDepositInstructionDiscriminator, &offset) + putUint16(data, args.AccountIndex, &offset) + putUint64(data, args.Amount, &offset) + + return Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmMemory, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmOmnibus, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.DepositorAta, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Depositor, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go index 2fcf783a..3b29dc60 100644 --- a/pkg/solana/cvm/utils.go +++ b/pkg/solana/cvm/utils.go @@ -73,6 +73,10 @@ func putUint32(dst []byte, v uint32, offset *int) { *offset += 4 } +func putUint64(dst []byte, v uint64, offset *int) { + binary.LittleEndian.PutUint64(dst[*offset:], v) + *offset += 8 +} func getUint64(src []byte, dst *uint64, offset *int) { *dst = binary.LittleEndian.Uint64(src[*offset:]) *offset += 8 From 36da0a80005bab6de60a638bce83e603dac5a9f4 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 3 Jul 2024 13:12:20 -0400 Subject: [PATCH 04/79] Add virtual instruction support starting with internal Timelock transfer (#146) --- pkg/solana/cvm/address.go | 2 +- .../cvm/instructions_system_nonce_init.go | 12 +- .../cvm/instructions_system_timelock_init.go | 12 +- .../cvm/instructions_timelock_deposit.go | 8 +- pkg/solana/cvm/instructions_vm_exec.go | 149 ++++++++++++++++++ pkg/solana/cvm/instructions_vm_init.go | 8 +- pkg/solana/cvm/instructions_vm_memory_init.go | 8 +- .../cvm/instructions_vm_memory_resize.go | 8 +- pkg/solana/cvm/legacy.go | 20 --- pkg/solana/cvm/program.go | 16 +- pkg/solana/cvm/transaction.go | 14 ++ pkg/solana/cvm/types_opcode.go | 12 ++ pkg/solana/cvm/types_page.go | 2 +- pkg/solana/cvm/utils.go | 22 ++- pkg/solana/cvm/virtual_instruction.go | 53 +++++++ ...instructions_timelock_transfer_internal.go | 61 +++++++ pkg/solana/ed25519/program.go | 62 ++++++++ 17 files changed, 411 insertions(+), 58 deletions(-) create mode 100644 pkg/solana/cvm/instructions_vm_exec.go delete mode 100644 pkg/solana/cvm/legacy.go create mode 100644 pkg/solana/cvm/transaction.go create mode 100644 pkg/solana/cvm/types_opcode.go create mode 100644 pkg/solana/cvm/virtual_instruction.go create mode 100644 pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go create mode 100644 pkg/solana/ed25519/program.go diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go index 6e53827f..cb41d8cf 100644 --- a/pkg/solana/cvm/address.go +++ b/pkg/solana/cvm/address.go @@ -14,7 +14,7 @@ const ( var ( CodeVmPrefix = []byte("code-vm") TimelockStateAccountPrefix = []byte("timelock_state") - TimelockVaultAccountPrefix = []byte("timelock_state") + TimelockVaultAccountPrefix = []byte("timelock_vault") VmMemoryAccountPrefix = []byte("vm_memory_account") VmOmnibusPrefix = []byte("vm_omnibus") VmUnlockPdaAccountPrefix = []byte("vm_unlock_pda_account") diff --git a/pkg/solana/cvm/instructions_system_nonce_init.go b/pkg/solana/cvm/instructions_system_nonce_init.go index 1afebf56..bfe5b637 100644 --- a/pkg/solana/cvm/instructions_system_nonce_init.go +++ b/pkg/solana/cvm/instructions_system_nonce_init.go @@ -1,6 +1,10 @@ package cvm -import "crypto/ed25519" +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) var SystemNonceInitInstructionDiscriminator = []byte{ 0x0d, 0xb2, 0x98, 0x60, 0xa7, 0x2c, 0x96, 0x30, @@ -24,7 +28,7 @@ type SystemNonceInitInstructionAccounts struct { func NewSystemNonceInitInstruction( accounts *SystemNonceInitInstructionAccounts, args *SystemNonceInitInstructionArgs, -) Instruction { +) solana.Instruction { var offset int // Serialize instruction arguments @@ -35,14 +39,14 @@ func NewSystemNonceInitInstruction( putDiscriminator(data, SystemNonceInitInstructionDiscriminator, &offset) putUint16(data, args.AccountIndex, &offset) - return Instruction{ + return solana.Instruction{ Program: PROGRAM_ADDRESS, // Instruction args Data: data, // Instruction accounts - Accounts: []AccountMeta{ + Accounts: []solana.AccountMeta{ { PublicKey: accounts.VmAuthority, IsWritable: true, diff --git a/pkg/solana/cvm/instructions_system_timelock_init.go b/pkg/solana/cvm/instructions_system_timelock_init.go index be6ae9ed..fe491d48 100644 --- a/pkg/solana/cvm/instructions_system_timelock_init.go +++ b/pkg/solana/cvm/instructions_system_timelock_init.go @@ -1,6 +1,10 @@ package cvm -import "crypto/ed25519" +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) var SystemTimelockInitInstructionDiscriminator = []byte{ 0x07, 0x0b, 0xf5, 0xc5, 0x68, 0xfe, 0xb7, 0xb6, @@ -30,7 +34,7 @@ type SystemTimelockInitInstructionAccounts struct { func NewSystemTimelockInitInstruction( accounts *SystemTimelockInitInstructionAccounts, args *SystemTimelockInitInstructionArgs, -) Instruction { +) solana.Instruction { var offset int // Serialize instruction arguments @@ -44,14 +48,14 @@ func NewSystemTimelockInitInstruction( putUint8(data, args.VirtualVaultBump, &offset) putUint8(data, args.VmUnlockPdaBump, &offset) - return Instruction{ + return solana.Instruction{ Program: PROGRAM_ADDRESS, // Instruction args Data: data, // Instruction accounts - Accounts: []AccountMeta{ + Accounts: []solana.AccountMeta{ { PublicKey: accounts.VmAuthority, IsWritable: true, diff --git a/pkg/solana/cvm/instructions_timelock_deposit.go b/pkg/solana/cvm/instructions_timelock_deposit.go index 206e5f79..d90898ba 100644 --- a/pkg/solana/cvm/instructions_timelock_deposit.go +++ b/pkg/solana/cvm/instructions_timelock_deposit.go @@ -2,6 +2,8 @@ package cvm import ( "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" ) var TimelockDepositInstructionDiscriminator = []byte{ @@ -30,7 +32,7 @@ type TimelockDepositInstructionAccounts struct { func NewTimelockDepositInstruction( accounts *TimelockDepositInstructionAccounts, args *TimelockDepositInstructionArgs, -) Instruction { +) solana.Instruction { var offset int // Serialize instruction arguments @@ -42,14 +44,14 @@ func NewTimelockDepositInstruction( putUint16(data, args.AccountIndex, &offset) putUint64(data, args.Amount, &offset) - return Instruction{ + return solana.Instruction{ Program: PROGRAM_ADDRESS, // Instruction args Data: data, // Instruction accounts - Accounts: []AccountMeta{ + Accounts: []solana.AccountMeta{ { PublicKey: accounts.VmAuthority, IsWritable: true, diff --git a/pkg/solana/cvm/instructions_vm_exec.go b/pkg/solana/cvm/instructions_vm_exec.go new file mode 100644 index 00000000..e830fb56 --- /dev/null +++ b/pkg/solana/cvm/instructions_vm_exec.go @@ -0,0 +1,149 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +var VmExecInstructionDiscriminator = []byte{ + 0xe5, 0xcf, 0x51, 0x74, 0xed, 0x96, 0xba, 0x3e, +} + +type VmExecArgsAndAccounts struct { + Args VmExecInstructionArgs + Accounts VmExecInstructionAccounts +} + +type VmExecInstructionArgs struct { + Opcode Opcode + MemIndices []uint16 + MemBanks []uint8 + SignatureIndex uint8 + Data []uint8 +} + +type VmExecInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemA *ed25519.PublicKey + VmMemB *ed25519.PublicKey + VmMemC *ed25519.PublicKey + VmMemD *ed25519.PublicKey + VmUnlockPda *ed25519.PublicKey + VmOmnibus *ed25519.PublicKey + VmRelay *ed25519.PublicKey + VmRelayVault *ed25519.PublicKey + ExternalAddress *ed25519.PublicKey +} + +func NewVmExecInstruction( + accounts *VmExecInstructionAccounts, + args *VmExecInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(VmExecInstructionDiscriminator)+ + getVmExecInstructionArgSize(args)) + + putDiscriminator(data, VmExecInstructionDiscriminator, &offset) + putOpcode(data, args.Opcode, &offset) + putUint16Array(data, args.MemIndices, &offset) + putUint8Array(data, args.MemBanks, &offset) + putUint8(data, args.SignatureIndex, &offset) + putUint8Array(data, args.Data, &offset) + + var tokenProgram *ed25519.PublicKey + if accounts.VmOmnibus != nil || accounts.ExternalAddress != nil { + tokenProgram = &SPL_TOKEN_PROGRAM_ID + } + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.VmMemA), + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.VmMemB), + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.VmMemC), + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.VmMemD), + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.VmUnlockPda), + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.VmOmnibus), + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.VmRelay), + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.VmRelayVault), + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.ExternalAddress), + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(tokenProgram), + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_IXNS_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} + +func getVmExecInstructionArgSize(args *VmExecInstructionArgs) int { + return (1 + // opcode + 4 + // len(mem_indices) + 2*len(args.MemIndices) + // mem_indices + 4 + // len(mem_banks) + len(args.MemBanks) + // mem_banks + 1 + // signature_index + 4 + // len(data) + len(args.Data)) // data +} diff --git a/pkg/solana/cvm/instructions_vm_init.go b/pkg/solana/cvm/instructions_vm_init.go index 34cd2d0c..2d14477d 100644 --- a/pkg/solana/cvm/instructions_vm_init.go +++ b/pkg/solana/cvm/instructions_vm_init.go @@ -2,6 +2,8 @@ package cvm import ( "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" ) var VmInitInstructionDiscriminator = []byte{ @@ -26,7 +28,7 @@ type VmInitInstructionAccounts struct { func NewVmInitInstruction( accounts *VmInitInstructionAccounts, args *VmInitInstructionArgs, -) Instruction { +) solana.Instruction { var offset int // Serialize instruction arguments @@ -37,14 +39,14 @@ func NewVmInitInstruction( putDiscriminator(data, VmInitInstructionDiscriminator, &offset) putUint8(data, args.LockDuration, &offset) - return Instruction{ + return solana.Instruction{ Program: PROGRAM_ADDRESS, // Instruction args Data: data, // Instruction accounts - Accounts: []AccountMeta{ + Accounts: []solana.AccountMeta{ { PublicKey: accounts.VmAuthority, IsWritable: true, diff --git a/pkg/solana/cvm/instructions_vm_memory_init.go b/pkg/solana/cvm/instructions_vm_memory_init.go index 72168079..30f40d9e 100644 --- a/pkg/solana/cvm/instructions_vm_memory_init.go +++ b/pkg/solana/cvm/instructions_vm_memory_init.go @@ -2,6 +2,8 @@ package cvm import ( "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" ) var VmMemoryInitInstructionDiscriminator = []byte{ @@ -26,7 +28,7 @@ type VmMemoryInitInstructionAccounts struct { func NewVmMemoryInitInstruction( accounts *VmMemoryInitInstructionAccounts, args *VmMemoryInitInstructionArgs, -) Instruction { +) solana.Instruction { var offset int // Serialize instruction arguments @@ -37,14 +39,14 @@ func NewVmMemoryInitInstruction( putDiscriminator(data, VmMemoryInitInstructionDiscriminator, &offset) putString(data, args.Name, &offset) - return Instruction{ + return solana.Instruction{ Program: PROGRAM_ADDRESS, // Instruction args Data: data, // Instruction accounts - Accounts: []AccountMeta{ + Accounts: []solana.AccountMeta{ { PublicKey: accounts.VmAuthority, IsWritable: true, diff --git a/pkg/solana/cvm/instructions_vm_memory_resize.go b/pkg/solana/cvm/instructions_vm_memory_resize.go index a0e98280..ad58dea1 100644 --- a/pkg/solana/cvm/instructions_vm_memory_resize.go +++ b/pkg/solana/cvm/instructions_vm_memory_resize.go @@ -2,6 +2,8 @@ package cvm import ( "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" ) var VmMemoryResizeInstructionDiscriminator = []byte{ @@ -25,7 +27,7 @@ type VmMemoryResizeInstructionAccounts struct { func NewVmMemoryResizeInstruction( accounts *VmMemoryResizeInstructionAccounts, args *VmMemoryResizeInstructionArgs, -) Instruction { +) solana.Instruction { var offset int // Serialize instruction arguments @@ -36,14 +38,14 @@ func NewVmMemoryResizeInstruction( putDiscriminator(data, VmMemoryResizeInstructionDiscriminator, &offset) putUint32(data, args.Len, &offset) - return Instruction{ + return solana.Instruction{ Program: PROGRAM_ADDRESS, // Instruction args Data: data, // Instruction accounts - Accounts: []AccountMeta{ + Accounts: []solana.AccountMeta{ { PublicKey: accounts.VmAuthority, IsWritable: true, diff --git a/pkg/solana/cvm/legacy.go b/pkg/solana/cvm/legacy.go deleted file mode 100644 index d3c9848d..00000000 --- a/pkg/solana/cvm/legacy.go +++ /dev/null @@ -1,20 +0,0 @@ -package cvm - -import "github.com/code-payments/code-server/pkg/solana" - -func (i Instruction) ToLegacyInstruction() solana.Instruction { - legacyAccountMeta := make([]solana.AccountMeta, len(i.Accounts)) - for i, accountMeta := range i.Accounts { - legacyAccountMeta[i] = solana.AccountMeta{ - PublicKey: accountMeta.PublicKey, - IsSigner: accountMeta.IsSigner, - IsWritable: accountMeta.IsWritable, - } - } - - return solana.Instruction{ - Program: PROGRAM_ID, - Accounts: legacyAccountMeta, - Data: i.Data, - } -} diff --git a/pkg/solana/cvm/program.go b/pkg/solana/cvm/program.go index f9b3d47e..8a9e21c5 100644 --- a/pkg/solana/cvm/program.go +++ b/pkg/solana/cvm/program.go @@ -24,20 +24,6 @@ var ( SPL_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) TIMELOCK_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("time2Z2SCnn3qYg3ULKVtdkh8YmZ5jFdKicnA1W2YnJ")) + SYSVAR_IXNS_PUBKEY = ed25519.PublicKey(mustBase58Decode("Sysvar1nstructions1111111111111111111111111")) SYSVAR_RENT_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarRent111111111111111111111111111111111")) ) - -// AccountMeta represents the account information required -// for building transactions. -type AccountMeta struct { - PublicKey ed25519.PublicKey - IsWritable bool - IsSigner bool -} - -// Instruction represents a transaction instruction. -type Instruction struct { - Program ed25519.PublicKey - Accounts []AccountMeta - Data []byte -} diff --git a/pkg/solana/cvm/transaction.go b/pkg/solana/cvm/transaction.go new file mode 100644 index 00000000..c450af31 --- /dev/null +++ b/pkg/solana/cvm/transaction.go @@ -0,0 +1,14 @@ +package cvm + +import ( + "crypto/ed25519" +) + +// todo: Utilities for crafting a transaction with virtual instructions, but this may be too low-level of a package for that + +func getOptionalAccountMetaAddress(account *ed25519.PublicKey) ed25519.PublicKey { + if account != nil { + return *account + } + return PROGRAM_ID +} diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go new file mode 100644 index 00000000..e20ada42 --- /dev/null +++ b/pkg/solana/cvm/types_opcode.go @@ -0,0 +1,12 @@ +package cvm + +type Opcode uint8 + +const ( + OpcodeTimelockTransferInternal Opcode = 36 +) + +func putOpcode(dst []byte, v Opcode, offset *int) { + dst[*offset] = uint8(v) + *offset += 1 +} diff --git a/pkg/solana/cvm/types_page.go b/pkg/solana/cvm/types_page.go index 2e796d30..40f005d2 100644 --- a/pkg/solana/cvm/types_page.go +++ b/pkg/solana/cvm/types_page.go @@ -26,7 +26,7 @@ func (obj *Page) Unmarshal(data []byte) error { obj.Data = make([]byte, PageDataLen) getBool(data, &obj.IsAllocated, &offset) - getData(data, obj.Data, PageDataLen, &offset) + getBytes(data, obj.Data, PageDataLen, &offset) getUint8(data, &obj.NextPage, &offset) return nil diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go index 3b29dc60..033a5b44 100644 --- a/pkg/solana/cvm/utils.go +++ b/pkg/solana/cvm/utils.go @@ -45,7 +45,7 @@ func getFixedString(data []byte, dst *string, length int, offset *int) { *offset += length } -func getData(src []byte, dst []byte, length int, offset *int) { +func getBytes(src []byte, dst []byte, length int, offset *int) { copy(dst[:length], src[*offset:*offset+length]) *offset += length } @@ -59,6 +59,16 @@ func getUint8(src []byte, dst *uint8, offset *int) { *offset += 1 } +func putUint8Array(dst []byte, array []uint8, offset *int) { + binary.LittleEndian.PutUint32(dst[*offset:], uint32(len(array))) + *offset += 4 + + for _, v := range array { + dst[*offset] = v + *offset += 1 + } +} + func putUint16(dst []byte, v uint16, offset *int) { binary.LittleEndian.PutUint16(dst[*offset:], v) *offset += 2 @@ -68,6 +78,16 @@ func getUint16(src []byte, dst *uint16, offset *int) { *offset += 2 } +func putUint16Array(dst []byte, array []uint16, offset *int) { + binary.LittleEndian.PutUint32(dst[*offset:], uint32(len(array))) + *offset += 4 + + for _, v := range array { + binary.LittleEndian.PutUint16(dst[*offset:], v) + *offset += 2 + } +} + func putUint32(dst []byte, v uint32, offset *int) { binary.LittleEndian.PutUint32(dst[*offset:], v) *offset += 4 diff --git a/pkg/solana/cvm/virtual_instruction.go b/pkg/solana/cvm/virtual_instruction.go new file mode 100644 index 00000000..ef74cd95 --- /dev/null +++ b/pkg/solana/cvm/virtual_instruction.go @@ -0,0 +1,53 @@ +package cvm + +import ( + "crypto/ed25519" + "crypto/sha256" + + "github.com/code-payments/code-server/pkg/solana" + solana_ed25519 "github.com/code-payments/code-server/pkg/solana/ed25519" + "github.com/code-payments/code-server/pkg/solana/system" +) + +// VirtualInstruction represents a virtual transaction instruction within the VM +type VirtualInstruction struct { + Opcode Opcode + Data []byte + Hash Hash +} + +type VirtualInstructionCtor func() (Opcode, []solana.Instruction, []byte) + +func NewVirtualInstruction( + vmAuthority ed25519.PublicKey, + nonce *VirtualDurableNonce, + vixnCtor VirtualInstructionCtor, +) VirtualInstruction { + opcode, ixns, data := vixnCtor() + ixns = append([]solana.Instruction{system.AdvanceNonce(nonce.Address, vmAuthority)}, ixns...) + txn := solana.NewTransaction( + vmAuthority, + ixns..., + ) + txn.SetBlockhash(solana.Blockhash(nonce.Nonce)) + + return VirtualInstruction{ + Opcode: opcode, + Data: data, + Hash: getTxnMessageHash(txn), + } +} + +func (i VirtualInstruction) GetEd25519Instruction(user ed25519.PrivateKey) solana.Instruction { + return solana_ed25519.Instruction(user, i.Hash[:]) +} + +func getTxnMessageHash(txn solana.Transaction) Hash { + msg := txn.Message.Marshal() + h := sha256.New() + h.Write(msg) + bytes := h.Sum(nil) + var typed Hash + copy(typed[:], bytes) + return typed +} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go new file mode 100644 index 00000000..b11ae28f --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go @@ -0,0 +1,61 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/memo" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" +) + +const ( + TimelockTransferInternalVirtrualInstructionDataSize = 8 // amount +) + +type TimelockTransferInternalVirtualInstructionArgs struct { + TimelockBump uint8 + Amount uint64 +} + +type TimelockTransferInternalVirtualInstructionAccounts struct { + VmAuthority ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + VirtualVault ed25519.PublicKey + Owner ed25519.PublicKey + Destination ed25519.PublicKey +} + +func NewTimelockTransferInternalVirtualInstructionCtor( + accounts *TimelockTransferInternalVirtualInstructionAccounts, + args *TimelockTransferInternalVirtualInstructionArgs, +) VirtualInstructionCtor { + return func() (Opcode, []solana.Instruction, []byte) { + var offset int + data := make([]byte, TimelockTransferInternalVirtrualInstructionDataSize) + putUint64(data, args.Amount, &offset) + + ixns := []solana.Instruction{ + newKreMemoIxn(), + timelock_token.NewTransferWithAuthorityInstruction( + &timelock_token.TransferWithAuthorityInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualVault, + VaultOwner: accounts.Owner, + TimeAuthority: accounts.VmAuthority, + Destination: accounts.Destination, + Payer: accounts.VmAuthority, + }, + &timelock_token.TransferWithAuthorityInstructionArgs{ + TimelockBump: args.TimelockBump, + Amount: args.Amount, + }, + ).ToLegacyInstruction(), + } + + return OpcodeTimelockTransferInternal, ixns, data + } +} + +func newKreMemoIxn() solana.Instruction { + return memo.Instruction("ZTAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") +} diff --git a/pkg/solana/ed25519/program.go b/pkg/solana/ed25519/program.go new file mode 100644 index 00000000..bc16c769 --- /dev/null +++ b/pkg/solana/ed25519/program.go @@ -0,0 +1,62 @@ +package ed25519 + +import ( + "crypto/ed25519" + "encoding/binary" + "math" + + "github.com/code-payments/code-server/pkg/solana" +) + +// Ed25519SigVerify111111111111111111111111111 +var ProgramKey = ed25519.PublicKey{3, 125, 70, 214, 124, 147, 251, 190, 18, 249, 66, 143, 131, 141, 64, 255, 5, 112, 116, 73, 39, 244, 138, 100, 252, 202, 112, 68, 128, 0, 0, 0} + +// Reference: https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/sdk/src/ed25519_instruction.rs#L32 +func Instruction(privateKey ed25519.PrivateKey, message []byte) solana.Instruction { + publicKey := privateKey.Public().(ed25519.PublicKey) + signature := ed25519.Sign(privateKey, message) + + data := make([]byte, 112+len(message)) + + offset := 0 + + data[offset] = 1 // num_signatures + offset++ + + data[offset] = 0 // padding + offset++ + + binary.LittleEndian.PutUint16(data[offset:], 48) // signature_offset + offset += 2 + + binary.LittleEndian.PutUint16(data[offset:], math.MaxUint16) // signature_instruction_index + offset += 2 + + binary.LittleEndian.PutUint16(data[offset:], 16) // public_key_offset + offset += 2 + + binary.LittleEndian.PutUint16(data[offset:], math.MaxUint16) // public_key_instruction_index + offset += 2 + + binary.LittleEndian.PutUint16(data[offset:], 112) // message_data_offset + offset += 2 + + binary.LittleEndian.PutUint16(data[offset:], uint16(len(message))) // message_data_size + offset += 2 + + binary.LittleEndian.PutUint16(data[offset:], math.MaxUint16) // message_instruction_index + offset += 2 + + copy(data[offset:], publicKey) + offset += 32 + + copy(data[offset:], signature) + offset += 64 + + copy(data[offset:], message) + + return solana.NewInstruction( + ProgramKey, + data, + ) +} From b6c29c86c1f324fea2874edf9aa0ffe8dbdce720 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 4 Jul 2024 08:26:32 -0400 Subject: [PATCH 05/79] Implement new VM deposit flow (#147) --- pkg/solana/cvm/address.go | 16 +++ ...instructions_timelock_deposit_from_ata.go} | 22 ++-- .../instructions_timelock_deposit_from_pda.go | 101 ++++++++++++++++++ 3 files changed, 128 insertions(+), 11 deletions(-) rename pkg/solana/cvm/{instructions_timelock_deposit.go => instructions_timelock_deposit_from_ata.go} (68%) create mode 100644 pkg/solana/cvm/instructions_timelock_deposit_from_pda.go diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go index cb41d8cf..b83535c6 100644 --- a/pkg/solana/cvm/address.go +++ b/pkg/solana/cvm/address.go @@ -17,6 +17,7 @@ var ( TimelockVaultAccountPrefix = []byte("timelock_vault") VmMemoryAccountPrefix = []byte("vm_memory_account") VmOmnibusPrefix = []byte("vm_omnibus") + VmDepositPdaPrefix = []byte("vm_deposit_pda") VmUnlockPdaAccountPrefix = []byte("vm_unlock_pda_account") VmWithdrawReceiptAccountPrefix = []byte("vm_withdraw_receipt_account") ) @@ -115,6 +116,21 @@ func GetVirtualTimelockVaultAddress(args *GetVirtualTimelockVaultAddressArgs) (e ) } +type GetVmDepositAddressArgs struct { + Depositor ed25519.PublicKey + Vm ed25519.PublicKey +} + +func GetVmDepositAddress(args *GetVmDepositAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmDepositPdaPrefix, + args.Depositor, + args.Vm, + ) +} + type GetVmUnlockStateAccountAddressArgs struct { Owner ed25519.PublicKey VirtualTimelock ed25519.PublicKey diff --git a/pkg/solana/cvm/instructions_timelock_deposit.go b/pkg/solana/cvm/instructions_timelock_deposit_from_ata.go similarity index 68% rename from pkg/solana/cvm/instructions_timelock_deposit.go rename to pkg/solana/cvm/instructions_timelock_deposit_from_ata.go index d90898ba..94bbe140 100644 --- a/pkg/solana/cvm/instructions_timelock_deposit.go +++ b/pkg/solana/cvm/instructions_timelock_deposit_from_ata.go @@ -6,21 +6,21 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var TimelockDepositInstructionDiscriminator = []byte{ - 0xe8, 0x19, 0x3f, 0x63, 0x5f, 0x15, 0xcc, 0xde, +var TimelockDepositFromAtaInstructionDiscriminator = []byte{ + 0xb8, 0x7a, 0x27, 0x63, 0x50, 0xc9, 0xa7, 0xd1, } const ( - TimelockDepositInstructionArgsSize = (2 + // account_index + TimelockDepositFromAtaInstructionArgsSize = (2 + // account_index 8) // amount ) -type TimelockDepositInstructionArgs struct { +type TimelockDepositFromAtaInstructionArgs struct { AccountIndex uint16 Amount uint64 } -type TimelockDepositInstructionAccounts struct { +type TimelockDepositFromAtaInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemory ed25519.PublicKey @@ -29,18 +29,18 @@ type TimelockDepositInstructionAccounts struct { Depositor ed25519.PublicKey } -func NewTimelockDepositInstruction( - accounts *TimelockDepositInstructionAccounts, - args *TimelockDepositInstructionArgs, +func NewTimelockDepositFromAtaInstruction( + accounts *TimelockDepositFromAtaInstructionAccounts, + args *TimelockDepositFromAtaInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments data := make([]byte, - len(TimelockDepositInstructionDiscriminator)+ - TimelockDepositInstructionArgsSize) + len(TimelockDepositFromAtaInstructionDiscriminator)+ + TimelockDepositFromAtaInstructionArgsSize) - putDiscriminator(data, TimelockDepositInstructionDiscriminator, &offset) + putDiscriminator(data, TimelockDepositFromAtaInstructionDiscriminator, &offset) putUint16(data, args.AccountIndex, &offset) putUint64(data, args.Amount, &offset) diff --git a/pkg/solana/cvm/instructions_timelock_deposit_from_pda.go b/pkg/solana/cvm/instructions_timelock_deposit_from_pda.go new file mode 100644 index 00000000..f63a8fcb --- /dev/null +++ b/pkg/solana/cvm/instructions_timelock_deposit_from_pda.go @@ -0,0 +1,101 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +var TimelockDepositFromPdaInstructionDiscriminator = []byte{ + 0x4c, 0xc5, 0xd9, 0x18, 0xb3, 0xe0, 0xdd, 0x9d, +} + +const ( + TimelockDepositFromPdaInstructionArgsSize = (2 + // account_index + 8 + //amount + 1) // bump +) + +type TimelockDepositFromPdaInstructionArgs struct { + AccountIndex uint16 + Amount uint64 + Bump uint8 +} + +type TimelockDepositFromPdaInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey + Depositor ed25519.PublicKey + DepositPda ed25519.PublicKey + DepositAta ed25519.PublicKey + VmOmnibus ed25519.PublicKey +} + +func NewTimelockDepositFromPdaInstruction( + accounts *TimelockDepositFromPdaInstructionAccounts, + args *TimelockDepositFromPdaInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(TimelockDepositFromPdaInstructionDiscriminator)+ + TimelockDepositFromPdaInstructionArgsSize) + + putDiscriminator(data, TimelockDepositFromPdaInstructionDiscriminator, &offset) + putUint16(data, args.AccountIndex, &offset) + putUint64(data, args.Amount, &offset) + putUint8(data, args.Bump, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmMemory, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Depositor, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.DepositPda, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.DepositAta, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmOmnibus, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + }, + } +} From 956a96cff39c4db8325ca5227723747af8d8eed0 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 4 Jul 2024 08:55:57 -0400 Subject: [PATCH 06/79] Add virtual instruction for external Timelock transfer (#148) --- pkg/solana/cvm/types_opcode.go | 1 + pkg/solana/cvm/virtual_instruction.go | 5 ++ ...instructions_timelock_transfer_external.go | 56 +++++++++++++++++++ ...instructions_timelock_transfer_internal.go | 5 -- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index e20ada42..53b18b83 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -4,6 +4,7 @@ type Opcode uint8 const ( OpcodeTimelockTransferInternal Opcode = 36 + OpcodeTimelockTransferExternal Opcode = 37 ) func putOpcode(dst []byte, v Opcode, offset *int) { diff --git a/pkg/solana/cvm/virtual_instruction.go b/pkg/solana/cvm/virtual_instruction.go index ef74cd95..dbfd012f 100644 --- a/pkg/solana/cvm/virtual_instruction.go +++ b/pkg/solana/cvm/virtual_instruction.go @@ -6,6 +6,7 @@ import ( "github.com/code-payments/code-server/pkg/solana" solana_ed25519 "github.com/code-payments/code-server/pkg/solana/ed25519" + "github.com/code-payments/code-server/pkg/solana/memo" "github.com/code-payments/code-server/pkg/solana/system" ) @@ -51,3 +52,7 @@ func getTxnMessageHash(txn solana.Transaction) Hash { copy(typed[:], bytes) return typed } + +func newKreMemoIxn() solana.Instruction { + return memo.Instruction("ZTAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") +} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go new file mode 100644 index 00000000..bf90d31e --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go @@ -0,0 +1,56 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" +) + +const ( + TimelockTransferExternalVirtrualInstructionDataSize = 8 // amount +) + +type TimelockTransferExternalVirtualInstructionArgs struct { + TimelockBump uint8 + Amount uint64 +} + +type TimelockTransferExternalVirtualInstructionAccounts struct { + VmAuthority ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + VirtualVault ed25519.PublicKey + Owner ed25519.PublicKey + Destination ed25519.PublicKey +} + +func NewTimelockTransferExternalVirtualInstructionCtor( + accounts *TimelockTransferExternalVirtualInstructionAccounts, + args *TimelockTransferExternalVirtualInstructionArgs, +) VirtualInstructionCtor { + return func() (Opcode, []solana.Instruction, []byte) { + var offset int + data := make([]byte, TimelockTransferExternalVirtrualInstructionDataSize) + putUint64(data, args.Amount, &offset) + + ixns := []solana.Instruction{ + newKreMemoIxn(), + timelock_token.NewTransferWithAuthorityInstruction( + &timelock_token.TransferWithAuthorityInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualVault, + VaultOwner: accounts.Owner, + TimeAuthority: accounts.VmAuthority, + Destination: accounts.Destination, + Payer: accounts.VmAuthority, + }, + &timelock_token.TransferWithAuthorityInstructionArgs{ + TimelockBump: args.TimelockBump, + Amount: args.Amount, + }, + ).ToLegacyInstruction(), + } + + return OpcodeTimelockTransferExternal, ixns, data + } +} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go index b11ae28f..248d0283 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go @@ -4,7 +4,6 @@ import ( "crypto/ed25519" "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/memo" timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) @@ -55,7 +54,3 @@ func NewTimelockTransferInternalVirtualInstructionCtor( return OpcodeTimelockTransferInternal, ixns, data } } - -func newKreMemoIxn() solana.Instruction { - return memo.Instruction("ZTAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") -} From d51b34287bd963cdaeec96919ee3f6ca64290bd9 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Fri, 5 Jul 2024 11:45:16 -0400 Subject: [PATCH 07/79] Support VM relay account management and the ability to advance treasury funds (#149) --- pkg/solana/cvm/accounts_relay.go | 88 +++++++++++++++++ pkg/solana/cvm/address.go | 64 +++++++++++- pkg/solana/cvm/instructions_relay_init.go | 99 +++++++++++++++++++ .../instructions_relay_save_recent_root.go | 74 ++++++++++++++ pkg/solana/cvm/instructions_vm_exec.go | 9 +- pkg/solana/cvm/instructions_vm_memory_init.go | 3 +- pkg/solana/cvm/types_hash.go | 27 +++++ pkg/solana/cvm/types_merkle_tree.go | 61 ++++++++++++ pkg/solana/cvm/types_opcode.go | 3 + pkg/solana/cvm/types_recent_hashes.go | 41 ++++++++ pkg/solana/cvm/types_token_pool.go | 44 +++++++++ pkg/solana/cvm/utils.go | 19 +++- pkg/solana/cvm/virtual_instruction.go | 23 +++-- ...al_instructions_relay_transfer_external.go | 39 ++++++++ ...al_instructions_relay_transfer_internal.go | 39 ++++++++ 15 files changed, 609 insertions(+), 24 deletions(-) create mode 100644 pkg/solana/cvm/accounts_relay.go create mode 100644 pkg/solana/cvm/instructions_relay_init.go create mode 100644 pkg/solana/cvm/instructions_relay_save_recent_root.go create mode 100644 pkg/solana/cvm/types_merkle_tree.go create mode 100644 pkg/solana/cvm/types_recent_hashes.go create mode 100644 pkg/solana/cvm/types_token_pool.go create mode 100644 pkg/solana/cvm/virtual_instructions_relay_transfer_external.go create mode 100644 pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go diff --git a/pkg/solana/cvm/accounts_relay.go b/pkg/solana/cvm/accounts_relay.go new file mode 100644 index 00000000..9cd134cb --- /dev/null +++ b/pkg/solana/cvm/accounts_relay.go @@ -0,0 +1,88 @@ +package cvm + +import ( + "bytes" + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const ( + MaxRelayAccountNameSize = 32 +) + +const ( + minRelayAccountSize = (8 + //discriminator + 32 + // vm + 1 + // bump + MaxRelayAccountNameSize + // name + 1 + // num_levels + 1 + // num_history + TokenPoolSize) // token_pool +) + +var RelayAccountDiscriminator = []byte{0xf2, 0xbb, 0xef, 0x5f, 0x89, 0xe1, 0xf5, 0x5c} + +type RelayAccount struct { + Vm ed25519.PublicKey + Bump uint8 + + Name string + NumLevels uint8 + NumHistory uint8 + + Treasury TokenPool + History MerkleTree + RecentHashes RecentHashes +} + +func (obj *RelayAccount) Unmarshal(data []byte) error { + if len(data) < minRelayAccountSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, RelayAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.Vm, &offset) + getUint8(data, &obj.Bump, &offset) + getString(data, &obj.Name, &offset) + getUint8(data, &obj.NumLevels, &offset) + getUint8(data, &obj.NumHistory, &offset) + + if len(data) < GetRelayAccountSize(int(obj.NumLevels), int(obj.NumHistory)) { + return ErrInvalidAccountData + } + + getTokenPool(data, &obj.Treasury, &offset) + getMerkleTree(data, &obj.History, &offset) + getRecentHashes(data, &obj.RecentHashes, &offset) + + return nil +} + +func (obj *RelayAccount) String() string { + return fmt.Sprintf( + "RelayAccount{vm=%s,bump=%d,name=%s,num_levels=%d,num_history=%d,treasury=%s,history=%s,recent_hashes=%s}", + base58.Encode(obj.Vm), + obj.Bump, + obj.Name, + obj.NumLevels, + obj.NumHistory, + obj.Treasury.String(), + obj.History.String(), + obj.RecentHashes.String(), + ) +} + +func GetRelayAccountSize(numLevels, numHistory int) int { + return (minRelayAccountSize + + +GetMerkleTreeSize(numLevels) + // history + +GetRecentHashesSize(numHistory)) // recent_hashes +} diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go index b83535c6..5b083376 100644 --- a/pkg/solana/cvm/address.go +++ b/pkg/solana/cvm/address.go @@ -3,6 +3,7 @@ package cvm import ( "crypto/ed25519" "crypto/sha256" + "encoding/binary" "github.com/code-payments/code-server/pkg/solana" ) @@ -13,11 +14,14 @@ const ( var ( CodeVmPrefix = []byte("code-vm") - TimelockStateAccountPrefix = []byte("timelock_state") - TimelockVaultAccountPrefix = []byte("timelock_vault") + RelayCommitmentPrefix = []byte("relay_commitment") + TimelockStatePrefix = []byte("timelock_state") + TimelockVaultPrefix = []byte("timelock_vault") VmMemoryAccountPrefix = []byte("vm_memory_account") VmOmnibusPrefix = []byte("vm_omnibus") VmDepositPdaPrefix = []byte("vm_deposit_pda") + VmRelayAccountPrefix = []byte("vm_relay_account") + VmRelayVaultPrefix = []byte("vm_relay_vault") VmUnlockPdaAccountPrefix = []byte("vm_unlock_pda_account") VmWithdrawReceiptAccountPrefix = []byte("vm_withdraw_receipt_account") ) @@ -70,6 +74,58 @@ func GetMemoryAccountAddress(args *GetMemoryAccountAddressArgs) (ed25519.PublicK ) } +type GetRelayAccountAddressArgs struct { + Name string + Vm ed25519.PublicKey +} + +func GetRelayAccountAddress(args *GetRelayAccountAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmRelayAccountPrefix, + []byte(args.Name), + args.Vm, + ) +} + +type GetRelayVaultAddressArgs struct { + Relay ed25519.PublicKey +} + +func GetRelayVaultAddress(args *GetRelayVaultAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmRelayVaultPrefix, + args.Relay, + ) +} + +type GetRelayCommitmentAddressArgs struct { + Relay ed25519.PublicKey + MerkleRoot Hash + Transcript Hash + Destination ed25519.PublicKey + Amount uint64 +} + +func GetRelayCommitmentAddress(args *GetRelayCommitmentAddressArgs) (ed25519.PublicKey, uint8, error) { + amountBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(amountBytes, args.Amount) + + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + RelayCommitmentPrefix, + args.Relay, + args.MerkleRoot[:], + args.Transcript[:], + args.Destination, + amountBytes, + ) +} + type GetVirtualDurableNonceAddressArgs struct { Seed ed25519.PublicKey Value Hash @@ -95,7 +151,7 @@ type GetVirtualTimelockAccountAddressArgs struct { func GetVirtualTimelockAccountAddress(args *GetVirtualTimelockAccountAddressArgs) (ed25519.PublicKey, uint8, error) { return solana.FindProgramAddressAndBump( TIMELOCK_PROGRAM_ID, - TimelockStateAccountPrefix, + TimelockStatePrefix, args.Mint, args.VmAuthority, args.Owner, @@ -110,7 +166,7 @@ type GetVirtualTimelockVaultAddressArgs struct { func GetVirtualTimelockVaultAddress(args *GetVirtualTimelockVaultAddressArgs) (ed25519.PublicKey, uint8, error) { return solana.FindProgramAddressAndBump( TIMELOCK_PROGRAM_ID, - TimelockVaultAccountPrefix, + TimelockVaultPrefix, args.VirtualTimelock, []byte{byte(TimelockDataVersion1)}, ) diff --git a/pkg/solana/cvm/instructions_relay_init.go b/pkg/solana/cvm/instructions_relay_init.go new file mode 100644 index 00000000..9f022276 --- /dev/null +++ b/pkg/solana/cvm/instructions_relay_init.go @@ -0,0 +1,99 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +var RelayInitInstructionDiscriminator = []byte{ + 0x72, 0x6a, 0x94, 0xd1, 0x3c, 0x83, 0xb4, 0xe1, +} + +const ( + RelayInitInstructionArgsSize = (1 + // num_levels + 1 + // num_history + MaxRelayAccountNameSize) // name +) + +type RelayInitInstructionArgs struct { + NumLevels uint8 + NumHistory uint8 + Name string +} + +type RelayInitInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + Relay ed25519.PublicKey + RelayVault ed25519.PublicKey + Mint ed25519.PublicKey +} + +func NewRelayInitInstruction( + accounts *RelayInitInstructionAccounts, + args *RelayInitInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(RelayInitInstructionDiscriminator)+ + RelayInitInstructionArgsSize) + + putDiscriminator(data, RelayInitInstructionDiscriminator, &offset) + putUint8(data, args.NumLevels, &offset) + putUint8(data, args.NumHistory, &offset) + putString(data, args.Name, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Relay, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.RelayVault, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Mint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/instructions_relay_save_recent_root.go b/pkg/solana/cvm/instructions_relay_save_recent_root.go new file mode 100644 index 00000000..6801c826 --- /dev/null +++ b/pkg/solana/cvm/instructions_relay_save_recent_root.go @@ -0,0 +1,74 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +var RelaySaveRecentRootInstructionDiscriminator = []byte{ + 0x9a, 0x54, 0x94, 0x43, 0x6e, 0xcc, 0x63, 0xcc, +} + +const ( + RelaySaveRecentRootInstructionArgsSize = 0 +) + +type RelaySaveRecentRootInstructionArgs struct { +} + +type RelaySaveRecentRootInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + Relay ed25519.PublicKey +} + +func NewRelaySaveRecentRootInstruction( + accounts *RelaySaveRecentRootInstructionAccounts, + args *RelaySaveRecentRootInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(RelaySaveRecentRootInstructionDiscriminator)+ + RelaySaveRecentRootInstructionArgsSize) + + putDiscriminator(data, RelaySaveRecentRootInstructionDiscriminator, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Relay, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/instructions_vm_exec.go b/pkg/solana/cvm/instructions_vm_exec.go index e830fb56..ab4b28ae 100644 --- a/pkg/solana/cvm/instructions_vm_exec.go +++ b/pkg/solana/cvm/instructions_vm_exec.go @@ -139,11 +139,8 @@ func NewVmExecInstruction( func getVmExecInstructionArgSize(args *VmExecInstructionArgs) int { return (1 + // opcode - 4 + // len(mem_indices) - 2*len(args.MemIndices) + // mem_indices - 4 + // len(mem_banks) - len(args.MemBanks) + // mem_banks + 4 + 2*len(args.MemIndices) + // mem_indices + 4 + len(args.MemBanks) + // mem_banks 1 + // signature_index - 4 + // len(data) - len(args.Data)) // data + 4 + len(args.Data)) // data } diff --git a/pkg/solana/cvm/instructions_vm_memory_init.go b/pkg/solana/cvm/instructions_vm_memory_init.go index 30f40d9e..1aac1011 100644 --- a/pkg/solana/cvm/instructions_vm_memory_init.go +++ b/pkg/solana/cvm/instructions_vm_memory_init.go @@ -11,8 +11,7 @@ var VmMemoryInitInstructionDiscriminator = []byte{ } const ( - VmMemoryInitInstructionArgsSize = (4 + // len(name) - 32) // name + VmMemoryInitInstructionArgsSize = (4 + MaxMemoryAccountNameLength) // name ) type VmMemoryInitInstructionArgs struct { diff --git a/pkg/solana/cvm/types_hash.go b/pkg/solana/cvm/types_hash.go index 3aa98d01..33e94ccf 100644 --- a/pkg/solana/cvm/types_hash.go +++ b/pkg/solana/cvm/types_hash.go @@ -1,7 +1,10 @@ package cvm import ( + "encoding/binary" "encoding/hex" + "fmt" + "strings" ) const HashSize = 32 @@ -12,7 +15,31 @@ func (h Hash) String() string { return hex.EncodeToString(h[:]) } +func putHash(dst []byte, v Hash, offset *int) { + copy(dst[*offset:], v[:]) + *offset += HashSize +} func getHash(src []byte, dst *Hash, offset *int) { copy(dst[:], src[*offset:]) *offset += HashSize } + +type HashArray []Hash + +func getHashArray(src []byte, dst *HashArray, offset *int) { + length := binary.LittleEndian.Uint32(src[*offset:]) + *offset += 4 + + *dst = make([]Hash, length) + for i := 0; i < int(length); i++ { + getHash(src, &(*dst)[i], offset) + } +} + +func (h HashArray) String() string { + stringValues := make([]string, len(h)) + for i := 0; i < len(h); i++ { + stringValues[i] = h[i].String() + } + return fmt.Sprintf("[%s]", strings.Join(stringValues, ",")) +} diff --git a/pkg/solana/cvm/types_merkle_tree.go b/pkg/solana/cvm/types_merkle_tree.go new file mode 100644 index 00000000..1cb61e05 --- /dev/null +++ b/pkg/solana/cvm/types_merkle_tree.go @@ -0,0 +1,61 @@ +package cvm + +import "fmt" + +const ( + MinMerkleTreeSize = 1 +) + +type MerkleTree struct { + Root Hash + Levels uint8 + NextIndex uint64 + + FilledSubtrees HashArray + ZeroValues HashArray +} + +func (obj *MerkleTree) Unmarshal(data []byte) error { + if len(data) < MinMerkleTreeSize { + return ErrInvalidAccountData + } + + var offset int + + getHash(data, &obj.Root, &offset) + getUint8(data, &obj.Levels, &offset) + + if len(data) < GetMerkleTreeSize(int(obj.Levels)) { + return ErrInvalidAccountData + } + + getUint64(data, &obj.NextIndex, &offset) + getHashArray(data, &obj.FilledSubtrees, &offset) + getHashArray(data, &obj.ZeroValues, &offset) + + return nil +} + +func (obj *MerkleTree) String() string { + return fmt.Sprintf( + "MerkleTree{root=%s,levels=%d,next_index=%d,filled_subtrees=%s,zero_values=%s}", + obj.Root.String(), + obj.Levels, + obj.NextIndex, + obj.FilledSubtrees.String(), + obj.ZeroValues.String(), + ) +} + +func getMerkleTree(src []byte, dst *MerkleTree, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += GetMerkleTreeSize(int(dst.Levels)) +} + +func GetMerkleTreeSize(levels int) int { + return (HashSize + // root + 1 + // levels + 8 + // next_index + 4 + levels*HashSize + // filled_subtrees + 4 + levels*HashSize) // zero_values +} diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index 53b18b83..206466ff 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -5,6 +5,9 @@ type Opcode uint8 const ( OpcodeTimelockTransferInternal Opcode = 36 OpcodeTimelockTransferExternal Opcode = 37 + + OpcodeTransferWithCommitmentInternal Opcode = 52 + OpcodeTransferWithCommitmentExternal Opcode = 53 ) func putOpcode(dst []byte, v Opcode, offset *int) { diff --git a/pkg/solana/cvm/types_recent_hashes.go b/pkg/solana/cvm/types_recent_hashes.go new file mode 100644 index 00000000..dee0b43b --- /dev/null +++ b/pkg/solana/cvm/types_recent_hashes.go @@ -0,0 +1,41 @@ +package cvm + +import ( + "fmt" +) + +type RecentHashes struct { + CurrentIndex uint8 + Items HashArray +} + +func (obj *RecentHashes) Unmarshal(data []byte) error { + if len(data) < 1 { + return ErrInvalidAccountData + } + + var offset int + + getUint8(data, &obj.CurrentIndex, &offset) + getHashArray(data, &obj.Items, &offset) + + return nil +} + +func (obj *RecentHashes) String() string { + return fmt.Sprintf( + "RecentHashes{current_index=%d,items=%s}", + obj.CurrentIndex, + obj.Items.String(), + ) +} + +func getRecentHashes(src []byte, dst *RecentHashes, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += GetRecentHashesSize(len(dst.Items)) +} + +func GetRecentHashesSize(numItems int) int { + return (1 + // current_index + 4 + numItems*HashSize) // items +} diff --git a/pkg/solana/cvm/types_token_pool.go b/pkg/solana/cvm/types_token_pool.go new file mode 100644 index 00000000..ec850c73 --- /dev/null +++ b/pkg/solana/cvm/types_token_pool.go @@ -0,0 +1,44 @@ +package cvm + +import ( + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const ( + TokenPoolSize = (32 + // vault + 1) // vault_bump +) + +type TokenPool struct { + Vault ed25519.PublicKey + VaultBump uint8 +} + +func (obj *TokenPool) Unmarshal(data []byte) error { + if len(data) < TokenPoolSize { + return ErrInvalidAccountData + } + + var offset int + + getKey(data, &obj.Vault, &offset) + getUint8(data, &obj.VaultBump, &offset) + + return nil +} + +func (obj *TokenPool) String() string { + return fmt.Sprintf( + "TokenPool{vault=%s,vault_bump=%d}", + base58.Encode(obj.Vault), + obj.VaultBump, + ) +} + +func getTokenPool(src []byte, dst *TokenPool, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += TokenPoolSize +} diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go index 033a5b44..49fbdd0e 100644 --- a/pkg/solana/cvm/utils.go +++ b/pkg/solana/cvm/utils.go @@ -18,6 +18,10 @@ func getDiscriminator(src []byte, dst *[]byte, offset *int) { *offset += 8 } +func putKey(dst []byte, v ed25519.PublicKey, offset *int) { + copy(dst[*offset:], v) + *offset += ed25519.PublicKeySize +} func getKey(src []byte, dst *ed25519.PublicKey, offset *int) { *dst = make([]byte, ed25519.PublicKeySize) copy(*dst, src[*offset:]) @@ -33,10 +37,17 @@ func getBool(src []byte, dst *bool, offset *int) { *offset += 1 } -func putString(dst []byte, src string, offset *int) { - putUint32(dst, uint32(len(src)), offset) - copy(dst[*offset:], src) - *offset += len(src) +func putString(dst []byte, v string, offset *int) { + putUint32(dst, uint32(len(v)), offset) + copy(dst[*offset:], v) + *offset += len(v) +} +func getString(data []byte, dst *string, offset *int) { + length := int(binary.LittleEndian.Uint32(data[*offset:])) + *offset += 4 + + *dst = string(data[*offset : *offset+length]) + *offset += length } func getFixedString(data []byte, dst *string, length int, offset *int) { diff --git a/pkg/solana/cvm/virtual_instruction.go b/pkg/solana/cvm/virtual_instruction.go index dbfd012f..35ef5926 100644 --- a/pkg/solana/cvm/virtual_instruction.go +++ b/pkg/solana/cvm/virtual_instruction.go @@ -14,7 +14,7 @@ import ( type VirtualInstruction struct { Opcode Opcode Data []byte - Hash Hash + Hash *Hash // Provided when user signature is required } type VirtualInstructionCtor func() (Opcode, []solana.Instruction, []byte) @@ -25,17 +25,24 @@ func NewVirtualInstruction( vixnCtor VirtualInstructionCtor, ) VirtualInstruction { opcode, ixns, data := vixnCtor() - ixns = append([]solana.Instruction{system.AdvanceNonce(nonce.Address, vmAuthority)}, ixns...) - txn := solana.NewTransaction( - vmAuthority, - ixns..., - ) - txn.SetBlockhash(solana.Blockhash(nonce.Nonce)) + + var hash *Hash + if len(ixns) > 0 { + ixns = append([]solana.Instruction{system.AdvanceNonce(nonce.Address, vmAuthority)}, ixns...) + txn := solana.NewTransaction( + vmAuthority, + ixns..., + ) + txn.SetBlockhash(solana.Blockhash(nonce.Nonce)) + + txnHash := getTxnMessageHash(txn) + hash = &txnHash + } return VirtualInstruction{ Opcode: opcode, Data: data, - Hash: getTxnMessageHash(txn), + Hash: hash, } } diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go new file mode 100644 index 00000000..a26839bb --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go @@ -0,0 +1,39 @@ +package cvm + +import ( + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + RelayTransferExternalVirtrualInstructionDataSize = (8 + // amount + HashSize + // transcript + HashSize + // recent_root + HashSize) // commitment +) + +type RelayTransferExternalVirtualInstructionArgs struct { + Amount uint64 + Transcript Hash + RecentRoot Hash + Commitment Hash +} + +type RelayTransferExternalVirtualInstructionAccounts struct { +} + +func NewRelayTransferExternalVirtualInstructionCtor( + accounts *RelayTransferExternalVirtualInstructionAccounts, + args *RelayTransferExternalVirtualInstructionArgs, +) VirtualInstructionCtor { + return func() (Opcode, []solana.Instruction, []byte) { + var offset int + data := make([]byte, RelayTransferExternalVirtrualInstructionDataSize) + + putUint64(data, args.Amount, &offset) + putHash(data, args.Transcript, &offset) + putHash(data, args.RecentRoot, &offset) + putHash(data, args.Commitment, &offset) + + return OpcodeTransferWithCommitmentExternal, nil, data + } +} diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go new file mode 100644 index 00000000..29fb5347 --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go @@ -0,0 +1,39 @@ +package cvm + +import ( + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + RelayTransferInternalVirtrualInstructionDataSize = (8 + // amount + HashSize + // transcript + HashSize + // recent_root + HashSize) // commitment +) + +type RelayTransferInternalVirtualInstructionArgs struct { + Amount uint64 + Transcript Hash + RecentRoot Hash + Commitment Hash +} + +type RelayTransferInternalVirtualInstructionAccounts struct { +} + +func NewRelayTransferInternalVirtualInstructionCtor( + accounts *RelayTransferInternalVirtualInstructionAccounts, + args *RelayTransferInternalVirtualInstructionArgs, +) VirtualInstructionCtor { + return func() (Opcode, []solana.Instruction, []byte) { + var offset int + data := make([]byte, RelayTransferInternalVirtrualInstructionDataSize) + + putUint64(data, args.Amount, &offset) + putHash(data, args.Transcript, &offset) + putHash(data, args.RecentRoot, &offset) + putHash(data, args.Commitment, &offset) + + return OpcodeTransferWithCommitmentInternal, nil, data + } +} From f9c9be330312d263415688d2b2e12f0fbc4cdf25 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Tue, 9 Jul 2024 09:40:01 -0400 Subject: [PATCH 08/79] Support cashing cheques from treasury advances with Timelock transfer through relay (#151) --- pkg/solana/cvm/address.go | 20 ++++++- pkg/solana/cvm/types_opcode.go | 1 + ...instructions_timelock_transfer_external.go | 12 ++-- ...instructions_timelock_transfer_internal.go | 12 ++-- ...al_instructions_timelock_transfer_relay.go | 56 +++++++++++++++++++ 5 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go index 5b083376..82f8815e 100644 --- a/pkg/solana/cvm/address.go +++ b/pkg/solana/cvm/address.go @@ -20,6 +20,7 @@ var ( VmMemoryAccountPrefix = []byte("vm_memory_account") VmOmnibusPrefix = []byte("vm_omnibus") VmDepositPdaPrefix = []byte("vm_deposit_pda") + VmProofAccountPrefix = []byte("vm_proof_account") VmRelayAccountPrefix = []byte("vm_relay_account") VmRelayVaultPrefix = []byte("vm_relay_vault") VmUnlockPdaAccountPrefix = []byte("vm_unlock_pda_account") @@ -90,7 +91,7 @@ func GetRelayAccountAddress(args *GetRelayAccountAddressArgs) (ed25519.PublicKey } type GetRelayVaultAddressArgs struct { - Relay ed25519.PublicKey + RelayOrProof ed25519.PublicKey } func GetRelayVaultAddress(args *GetRelayVaultAddressArgs) (ed25519.PublicKey, uint8, error) { @@ -98,7 +99,24 @@ func GetRelayVaultAddress(args *GetRelayVaultAddressArgs) (ed25519.PublicKey, ui PROGRAM_ID, CodeVmPrefix, VmRelayVaultPrefix, + args.RelayOrProof, + ) +} + +type GetRelayProofAddressArgs struct { + Relay ed25519.PublicKey + MerkleRoot Hash + Commitment Hash +} + +func GetRelayProofAddress(args *GetRelayProofAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmProofAccountPrefix, args.Relay, + args.MerkleRoot[:], + args.Commitment[:], ) } diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index 206466ff..321c16ed 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -5,6 +5,7 @@ type Opcode uint8 const ( OpcodeTimelockTransferInternal Opcode = 36 OpcodeTimelockTransferExternal Opcode = 37 + OpcodeTimelockTransferRelay Opcode = 38 OpcodeTransferWithCommitmentInternal Opcode = 52 OpcodeTransferWithCommitmentExternal Opcode = 53 diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go index bf90d31e..74a744f4 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go @@ -17,11 +17,11 @@ type TimelockTransferExternalVirtualInstructionArgs struct { } type TimelockTransferExternalVirtualInstructionAccounts struct { - VmAuthority ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - VirtualVault ed25519.PublicKey - Owner ed25519.PublicKey - Destination ed25519.PublicKey + VmAuthority ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + VirtualTimelockVault ed25519.PublicKey + Owner ed25519.PublicKey + Destination ed25519.PublicKey } func NewTimelockTransferExternalVirtualInstructionCtor( @@ -38,7 +38,7 @@ func NewTimelockTransferExternalVirtualInstructionCtor( timelock_token.NewTransferWithAuthorityInstruction( &timelock_token.TransferWithAuthorityInstructionAccounts{ Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualVault, + Vault: accounts.VirtualTimelockVault, VaultOwner: accounts.Owner, TimeAuthority: accounts.VmAuthority, Destination: accounts.Destination, diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go index 248d0283..d662b897 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go @@ -17,11 +17,11 @@ type TimelockTransferInternalVirtualInstructionArgs struct { } type TimelockTransferInternalVirtualInstructionAccounts struct { - VmAuthority ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - VirtualVault ed25519.PublicKey - Owner ed25519.PublicKey - Destination ed25519.PublicKey + VmAuthority ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + VirtualTimelockVault ed25519.PublicKey + Owner ed25519.PublicKey + Destination ed25519.PublicKey } func NewTimelockTransferInternalVirtualInstructionCtor( @@ -38,7 +38,7 @@ func NewTimelockTransferInternalVirtualInstructionCtor( timelock_token.NewTransferWithAuthorityInstruction( &timelock_token.TransferWithAuthorityInstructionAccounts{ Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualVault, + Vault: accounts.VirtualTimelockVault, VaultOwner: accounts.Owner, TimeAuthority: accounts.VmAuthority, Destination: accounts.Destination, diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go new file mode 100644 index 00000000..2766a4ca --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go @@ -0,0 +1,56 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" +) + +const ( + TimelockTransferRelayVirtrualInstructionDataSize = 8 // amount +) + +type TimelockTransferRelayVirtualInstructionArgs struct { + TimelockBump uint8 + Amount uint64 +} + +type TimelockTransferRelayVirtualInstructionAccounts struct { + VmAuthority ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + VirtualTimelockVault ed25519.PublicKey + Owner ed25519.PublicKey + RelayVault ed25519.PublicKey +} + +func NewTimelockTransferRelayVirtualInstructionCtor( + accounts *TimelockTransferRelayVirtualInstructionAccounts, + args *TimelockTransferRelayVirtualInstructionArgs, +) VirtualInstructionCtor { + return func() (Opcode, []solana.Instruction, []byte) { + var offset int + data := make([]byte, TimelockTransferRelayVirtrualInstructionDataSize) + putUint64(data, args.Amount, &offset) + + ixns := []solana.Instruction{ + newKreMemoIxn(), + timelock_token.NewTransferWithAuthorityInstruction( + &timelock_token.TransferWithAuthorityInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + VaultOwner: accounts.Owner, + TimeAuthority: accounts.VmAuthority, + Destination: accounts.RelayVault, + Payer: accounts.VmAuthority, + }, + &timelock_token.TransferWithAuthorityInstructionArgs{ + TimelockBump: args.TimelockBump, + Amount: args.Amount, + }, + ).ToLegacyInstruction(), + } + + return OpcodeTimelockTransferRelay, ixns, data + } +} From 9d30075e3f8bb2b20fd056a1feccdb193406382d Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 15 Jul 2024 12:57:58 -0400 Subject: [PATCH 09/79] VM memory rename updates (#152) --- pkg/solana/cvm/accounts_memory_account.go | 6 +- pkg/solana/cvm/types_account_buffer.go | 86 ----------------------- pkg/solana/cvm/types_account_index.go | 47 ------------- pkg/solana/cvm/types_allocated_memory.go | 47 +++++++++++++ pkg/solana/cvm/types_paged_memory.go | 86 +++++++++++++++++++++++ 5 files changed, 136 insertions(+), 136 deletions(-) delete mode 100644 pkg/solana/cvm/types_account_buffer.go delete mode 100644 pkg/solana/cvm/types_account_index.go create mode 100644 pkg/solana/cvm/types_allocated_memory.go create mode 100644 pkg/solana/cvm/types_paged_memory.go diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index b181c121..69f40058 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -16,7 +16,7 @@ type MemoryAccountWithData struct { Vm ed25519.PublicKey Bump uint8 Name string - Data AccountBuffer + Data PagedMemory } const MemoryAccountWithDataSize = (8 + // discriminator @@ -24,7 +24,7 @@ const MemoryAccountWithDataSize = (8 + // discriminator 1 + // bump MaxMemoryAccountNameLength + // name 1 + // padding - AccountBufferSize) // data + PagedMemorySize) // data var MemoryAccountDiscriminator = []byte{0x89, 0x7a, 0xdc, 0x6e, 0xdd, 0xca, 0x3e, 0x7f} @@ -45,7 +45,7 @@ func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { getUint8(data, &obj.Bump, &offset) getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) offset += 1 // padding - getAccountBuffer(data, &obj.Data, &offset) + getPagedMemory(data, &obj.Data, &offset) return nil } diff --git a/pkg/solana/cvm/types_account_buffer.go b/pkg/solana/cvm/types_account_buffer.go deleted file mode 100644 index 74752d97..00000000 --- a/pkg/solana/cvm/types_account_buffer.go +++ /dev/null @@ -1,86 +0,0 @@ -package cvm - -import ( - "fmt" - "strings" -) - -const ( - NumAccounts = 100 - NumSectors = 2 -) - -const AccountBufferSize = (NumAccounts*AccountIndexSize + // accounts - NumSectors*SectorSize) // sectors - -type AccountBuffer struct { - Accounts []AccountIndex - Sectors []Sector -} - -func (obj *AccountBuffer) ReadAccount(index int) ([]byte, bool) { - if index >= len(obj.Accounts) { - return nil, false - } - - account := obj.Accounts[index] - if !account.IsAllocated() { - return nil, false - } - - pages := obj.Sectors[account.Sector].GetLinkedPages(account.Page) - - var data []byte - for _, page := range pages { - if !page.IsAllocated { - return nil, false - } - - data = append(data, page.Data...) - } - - return data[:account.Size], true -} - -func (obj *AccountBuffer) Unmarshal(data []byte) error { - if len(data) < AccountBufferSize { - return ErrInvalidAccountData - } - - var offset int - - obj.Accounts = make([]AccountIndex, NumAccounts) - obj.Sectors = make([]Sector, NumSectors) - - for i := 0; i < NumAccounts; i++ { - getAccountIndex(data, &obj.Accounts[i], &offset) - } - for i := 0; i < NumSectors; i++ { - getSector(data, &obj.Sectors[i], &offset) - } - - return nil -} - -func (obj *AccountBuffer) String() string { - accountStrings := make([]string, len(obj.Accounts)) - for i, account := range obj.Accounts { - accountStrings[i] = account.String() - } - - sectorStrings := make([]string, len(obj.Sectors)) - for i, page := range obj.Sectors { - sectorStrings[i] = page.String() - } - - return fmt.Sprintf( - "Sector{accounts=[%s],sectors=[%s]}", - strings.Join(accountStrings, ","), - strings.Join(sectorStrings, ","), - ) -} - -func getAccountBuffer(src []byte, dst *AccountBuffer, offset *int) { - dst.Unmarshal(src[*offset:]) - *offset += AccountBufferSize -} diff --git a/pkg/solana/cvm/types_account_index.go b/pkg/solana/cvm/types_account_index.go deleted file mode 100644 index 16e4a0d1..00000000 --- a/pkg/solana/cvm/types_account_index.go +++ /dev/null @@ -1,47 +0,0 @@ -package cvm - -import ( - "fmt" -) - -const AccountIndexSize = (2 + // size - 1 + // page - 1) // sector - -type AccountIndex struct { - Size uint16 - Page uint8 - Sector uint8 -} - -func (i *AccountIndex) IsAllocated() bool { - return i.Size != 0 -} - -func (obj *AccountIndex) Unmarshal(data []byte) error { - if len(data) < AccountIndexSize { - return ErrInvalidAccountData - } - - var offset int - - getUint16(data, &obj.Size, &offset) - getUint8(data, &obj.Page, &offset) - getUint8(data, &obj.Sector, &offset) - - return nil -} - -func (obj *AccountIndex) String() string { - return fmt.Sprintf( - "AccountIndex{size=%d,page=%d,sector=%d}", - obj.Size, - obj.Page, - obj.Sector, - ) -} - -func getAccountIndex(src []byte, dst *AccountIndex, offset *int) { - dst.Unmarshal(src[*offset:]) - *offset += AccountIndexSize -} diff --git a/pkg/solana/cvm/types_allocated_memory.go b/pkg/solana/cvm/types_allocated_memory.go new file mode 100644 index 00000000..07e107f5 --- /dev/null +++ b/pkg/solana/cvm/types_allocated_memory.go @@ -0,0 +1,47 @@ +package cvm + +import ( + "fmt" +) + +const AllocatedMemorySize = (2 + // size + 1 + // page + 1) // sector + +type AllocatedMemory struct { + Size uint16 + Page uint8 + Sector uint8 +} + +func (obj *AllocatedMemory) IsAllocated() bool { + return obj.Size != 0 +} + +func (obj *AllocatedMemory) Unmarshal(data []byte) error { + if len(data) < AllocatedMemorySize { + return ErrInvalidAccountData + } + + var offset int + + getUint16(data, &obj.Size, &offset) + getUint8(data, &obj.Page, &offset) + getUint8(data, &obj.Sector, &offset) + + return nil +} + +func (obj *AllocatedMemory) String() string { + return fmt.Sprintf( + "AllocatedMemory{size=%d,page=%d,sector=%d}", + obj.Size, + obj.Page, + obj.Sector, + ) +} + +func getAllocatedMemory(src []byte, dst *AllocatedMemory, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += AllocatedMemorySize +} diff --git a/pkg/solana/cvm/types_paged_memory.go b/pkg/solana/cvm/types_paged_memory.go new file mode 100644 index 00000000..30c0dff1 --- /dev/null +++ b/pkg/solana/cvm/types_paged_memory.go @@ -0,0 +1,86 @@ +package cvm + +import ( + "fmt" + "strings" +) + +const ( + PageCapacity = 100 + NumSectors = 2 +) + +const PagedMemorySize = (PageCapacity*AllocatedMemorySize + // accounts + NumSectors*SectorSize) // sectors + +type PagedMemory struct { + Items []AllocatedMemory + Sectors []Sector +} + +func (obj *PagedMemory) Read(index int) ([]byte, bool) { + if index >= len(obj.Items) { + return nil, false + } + + account := obj.Items[index] + if !account.IsAllocated() { + return nil, false + } + + pages := obj.Sectors[account.Sector].GetLinkedPages(account.Page) + + var data []byte + for _, page := range pages { + if !page.IsAllocated { + return nil, false + } + + data = append(data, page.Data...) + } + + return data[:account.Size], true +} + +func (obj *PagedMemory) Unmarshal(data []byte) error { + if len(data) < PagedMemorySize { + return ErrInvalidAccountData + } + + var offset int + + obj.Items = make([]AllocatedMemory, PageCapacity) + obj.Sectors = make([]Sector, NumSectors) + + for i := 0; i < PageCapacity; i++ { + getAllocatedMemory(data, &obj.Items[i], &offset) + } + for i := 0; i < NumSectors; i++ { + getSector(data, &obj.Sectors[i], &offset) + } + + return nil +} + +func (obj *PagedMemory) String() string { + itemStrings := make([]string, len(obj.Items)) + for i, item := range obj.Items { + itemStrings[i] = item.String() + } + + sectorStrings := make([]string, len(obj.Sectors)) + for i, page := range obj.Sectors { + sectorStrings[i] = page.String() + } + + return fmt.Sprintf( + "PagedMemory{items=[%s],sectors=[%s]}", + strings.Join(itemStrings, ","), + strings.Join(sectorStrings, ","), + ) +} + +func getPagedMemory(src []byte, dst *PagedMemory, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += PagedMemorySize +} From 555f4d7fdc8fa222882c3ab188dd8e592d3d976e Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 17 Jul 2024 11:31:27 -0400 Subject: [PATCH 10/79] Add VM virtual account Marshal utilities (#153) --- pkg/solana/cvm/virtual_accounts_durable_nonce.go | 11 +++++++++++ pkg/solana/cvm/virtual_accounts_relay_account.go | 13 +++++++++++++ .../cvm/virtual_accounts_timelock_account.go | 16 ++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/pkg/solana/cvm/virtual_accounts_durable_nonce.go b/pkg/solana/cvm/virtual_accounts_durable_nonce.go index 5d1eb454..4e4d48ec 100644 --- a/pkg/solana/cvm/virtual_accounts_durable_nonce.go +++ b/pkg/solana/cvm/virtual_accounts_durable_nonce.go @@ -15,6 +15,17 @@ type VirtualDurableNonce struct { Nonce Hash } +func (obj *VirtualDurableNonce) Marshal() []byte { + data := make([]byte, VirtualDurableNonceSize) + + var offset int + + putKey(data, obj.Address, &offset) + putHash(data, obj.Nonce, &offset) + + return data +} + func (obj *VirtualDurableNonce) UnmarshalDirectly(data []byte) error { if len(data) < VirtualDurableNonceSize { return ErrInvalidVirtualAccountData diff --git a/pkg/solana/cvm/virtual_accounts_relay_account.go b/pkg/solana/cvm/virtual_accounts_relay_account.go index 1186603b..51e1ebb4 100644 --- a/pkg/solana/cvm/virtual_accounts_relay_account.go +++ b/pkg/solana/cvm/virtual_accounts_relay_account.go @@ -19,6 +19,19 @@ type VirtualRelayAccount struct { Destination ed25519.PublicKey } +func (obj *VirtualRelayAccount) Marshal() []byte { + data := make([]byte, VirtualRelayAccountSize) + + var offset int + + putKey(data, obj.Address, &offset) + putHash(data, obj.Commitment, &offset) + putHash(data, obj.RecentRoot, &offset) + putKey(data, obj.Destination, &offset) + + return data +} + func (obj *VirtualRelayAccount) UnmarshalDirectly(data []byte) error { if len(data) < VirtualRelayAccountSize { return ErrInvalidVirtualAccountData diff --git a/pkg/solana/cvm/virtual_accounts_timelock_account.go b/pkg/solana/cvm/virtual_accounts_timelock_account.go index 282004da..dae46f63 100644 --- a/pkg/solana/cvm/virtual_accounts_timelock_account.go +++ b/pkg/solana/cvm/virtual_accounts_timelock_account.go @@ -27,6 +27,22 @@ type VirtualTimelockAccount struct { Bump uint8 } +func (obj *VirtualTimelockAccount) Marshal() []byte { + data := make([]byte, VirtualTimelockAccountSize) + + var offset int + + putKey(data, obj.Owner, &offset) + putHash(data, obj.Nonce, &offset) + putUint8(data, obj.TokenBump, &offset) + putUint8(data, obj.UnlockBump, &offset) + putUint8(data, obj.WithdrawBump, &offset) + putUint64(data, obj.Balance, &offset) + putUint8(data, obj.Bump, &offset) + + return data +} + func (obj *VirtualTimelockAccount) UnmarshalDirectly(data []byte) error { if len(data) < VirtualTimelockAccountSize { return ErrInvalidVirtualAccountData From a570d1c3feb41f99e7629a8d1f6cb254e6987a7c Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Tue, 23 Jul 2024 15:04:42 -0400 Subject: [PATCH 11/79] VM nonce worker (#154) * Include VM as an optional field to the nonce record * Generalize nonce record to include an environment and instance instead of a VM field * Update nonce store interface to be environment aware * Fix common package * Fix SelectAvailableNonce * Fix commitment worker * Fix treasury worker * Fix sequencer worker * Fix transaction gRPC service * Fix nonce worker for nonces on Solana mainnet * Add main available/released nonce worker flow for VDN * Setup CVM nonce workers with refactored configs --- go.mod | 11 +- go.sum | 18 +-- pkg/code/async/commitment/testutil.go | 12 +- pkg/code/async/commitment/worker.go | 12 +- pkg/code/async/nonce/allocator.go | 17 ++- pkg/code/async/nonce/config.go | 39 ++++++ pkg/code/async/nonce/keys.go | 4 +- pkg/code/async/nonce/metrics.go | 19 ++- pkg/code/async/nonce/pool.go | 84 ++++++++----- pkg/code/async/nonce/service.go | 74 +++++------- pkg/code/async/nonce/util.go | 59 +++++++++- .../sequencer/fulfillment_handler_test.go | 26 ++-- pkg/code/async/sequencer/worker.go | 10 +- pkg/code/async/sequencer/worker_test.go | 36 +++--- pkg/code/async/treasury/recent_root.go | 10 +- pkg/code/async/treasury/testutil.go | 24 ++-- pkg/code/common/subsidizer.go | 8 +- pkg/code/common/subsidizer_test.go | 4 +- pkg/code/data/internal.go | 30 ++--- pkg/code/data/nonce/memory/store.go | 47 +++++--- pkg/code/data/nonce/nonce.go | 65 ++++++++-- pkg/code/data/nonce/postgres/model.go | 100 ++++++++-------- pkg/code/data/nonce/postgres/store.go | 37 ++---- pkg/code/data/nonce/postgres/store_test.go | 4 + pkg/code/data/nonce/store.go | 24 ++-- pkg/code/data/nonce/tests/tests.go | 111 ++++++++++-------- .../server/grpc/transaction/v2/airdrop.go | 2 +- pkg/code/server/grpc/transaction/v2/intent.go | 2 +- .../server/grpc/transaction/v2/testutil.go | 16 +-- pkg/code/transaction/nonce.go | 14 +-- pkg/code/transaction/nonce_test.go | 57 +++++---- 31 files changed, 599 insertions(+), 377 deletions(-) create mode 100644 pkg/code/async/nonce/config.go diff --git a/go.mod b/go.mod index 8701d46c..c08dee34 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,13 @@ module github.com/code-payments/code-server -go 1.21.3 +go 1.21.6 require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 github.com/code-payments/code-protobuf-api v1.16.6 + github.com/code-payments/code-vm-indexer v0.0.0-20240722205247-52cedd8b587d github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 @@ -44,13 +45,13 @@ require ( golang.org/x/text v0.14.0 golang.org/x/time v0.5.0 google.golang.org/api v0.170.0 - google.golang.org/grpc v1.62.1 + google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 ) require ( cloud.google.com/go v0.112.0 // indirect - cloud.google.com/go/compute v1.23.4 // indirect + cloud.google.com/go/compute v1.24.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/firestore v1.14.0 // indirect cloud.google.com/go/iam v1.1.6 // indirect @@ -123,8 +124,8 @@ require ( golang.org/x/sys v0.18.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine/v2 v2.0.1 // indirect - google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect gopkg.in/ini.v1 v1.51.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index ca244823..694baadf 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTB cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.23.4 h1:EBT9Nw4q3zyE7G45Wvv3MzolIrCJEuHys5muLY0wvAw= -cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOkkfQZa4ioI= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -123,6 +123,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-protobuf-api v1.16.6 h1:QCot0U+4Ar5SdSX4v955FORMsd3Qcf0ZgkoqlGJZzu0= github.com/code-payments/code-protobuf-api v1.16.6/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-vm-indexer v0.0.0-20240722205247-52cedd8b587d h1:itYJseNRSiz/wreM40eUpBDKTmgIsGffgw7+Ls8YKeA= +github.com/code-payments/code-vm-indexer v0.0.0-20240722205247-52cedd8b587d/go.mod h1:zkX5TSEOWYTcr5c1OhLHEVcYbW6UePp/szWVPBI0wPQ= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -1043,10 +1045,10 @@ google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 h1:g/4bk7P6TPMkAUbUhquq98xey1slwvuVJPosdBqYJlU= -google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M= -google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A= -google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1077,8 +1079,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/pkg/code/async/commitment/testutil.go b/pkg/code/async/commitment/testutil.go index 12490f92..8aa85366 100644 --- a/pkg/code/async/commitment/testutil.go +++ b/pkg/code/async/commitment/testutil.go @@ -501,11 +501,13 @@ func (e *testEnv) generateAvailableNonce(t *testing.T) *nonce.Record { CreatedAt: time.Now(), } nonceRecord := &nonce.Record{ - Address: nonceAccount.PublicKey().ToBase58(), - Authority: e.subsidizer.PublicKey().ToBase58(), - Blockhash: base58.Encode(bh[:]), - Purpose: nonce.PurposeInternalServerProcess, - State: nonce.StateAvailable, + Address: nonceAccount.PublicKey().ToBase58(), + Authority: e.subsidizer.PublicKey().ToBase58(), + Blockhash: base58.Encode(bh[:]), + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeInternalServerProcess, + State: nonce.StateAvailable, } require.NoError(t, e.data.SaveKey(e.ctx, nonceKey)) require.NoError(t, e.data.SaveNonce(e.ctx, nonceRecord)) diff --git a/pkg/code/async/commitment/worker.go b/pkg/code/async/commitment/worker.go index 496503d6..75318b19 100644 --- a/pkg/code/async/commitment/worker.go +++ b/pkg/code/async/commitment/worker.go @@ -11,11 +11,6 @@ import ( "github.com/mr-tron/base58" "github.com/newrelic/go-agent/v3/newrelic" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/retry" - "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/commitment" @@ -24,6 +19,11 @@ import ( "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/treasury" "github.com/code-payments/code-server/pkg/code/transaction" + "github.com/code-payments/code-server/pkg/database/query" + "github.com/code-payments/code-server/pkg/metrics" + "github.com/code-payments/code-server/pkg/pointer" + "github.com/code-payments/code-server/pkg/retry" + "github.com/code-payments/code-server/pkg/solana" ) // @@ -420,7 +420,7 @@ func (p *service) injectCommitmentVaultManagementFulfillments(ctx context.Contex )}, {fulfillment.CloseCommitmentVault, makeCloseCommitmentVaultInstructions(txnAccounts, txnArgs)}, } { - selectedNonce, err := transaction.SelectAvailableNonce(ctx, p.data, nonce.PurposeInternalServerProcess) + selectedNonce, err := transaction.SelectAvailableNonce(ctx, p.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) if err != nil { return err } diff --git a/pkg/code/async/nonce/allocator.go b/pkg/code/async/nonce/allocator.go index cebee7be..d9240eb8 100644 --- a/pkg/code/async/nonce/allocator.go +++ b/pkg/code/async/nonce/allocator.go @@ -11,7 +11,12 @@ import ( "github.com/code-payments/code-server/pkg/retry" ) -func (p *service) generateNonceAccounts(serviceCtx context.Context) error { +// todo: Add process for allocating VDN, which has some key differences: +// - Don't know the address in advance +// - Need some level of memory account management with the ability to find a free index +// - Does not require a vault key record + +func (p *service) generateNonceAccountsOnSolanaMainnet(serviceCtx context.Context) error { hasWarnedUser := false err := retry.Loop( @@ -23,7 +28,7 @@ func (p *service) generateNonceAccounts(serviceCtx context.Context) error { defer m.End() tracedCtx := newrelic.NewContext(serviceCtx, m) - num_invalid, err := p.data.GetNonceCountByState(tracedCtx, nonce.StateInvalid) + num_invalid, err := p.data.GetNonceCountByState(tracedCtx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateInvalid) if err != nil { return err } @@ -33,17 +38,17 @@ func (p *service) generateNonceAccounts(serviceCtx context.Context) error { return ErrInvalidNonceLimitExceeded } - num_available, err := p.data.GetNonceCountByState(tracedCtx, nonce.StateAvailable) + num_available, err := p.data.GetNonceCountByState(tracedCtx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateAvailable) if err != nil { return err } - num_released, err := p.data.GetNonceCountByState(tracedCtx, nonce.StateReleased) + num_released, err := p.data.GetNonceCountByState(tracedCtx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReleased) if err != nil { return err } - num_unknown, err := p.data.GetNonceCountByState(tracedCtx, nonce.StateUnknown) + num_unknown, err := p.data.GetNonceCountByState(tracedCtx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateUnknown) if err != nil { return err } @@ -51,7 +56,7 @@ func (p *service) generateNonceAccounts(serviceCtx context.Context) error { // Get a count of nonces that are available or potentially available // within a short amount of time. num_potentially_available := num_available + num_released + num_unknown - if num_potentially_available >= uint64(p.size) { + if num_potentially_available >= p.conf.solanaMainnetNoncePoolSize.Get(serviceCtx) { if hasWarnedUser { p.log.Warn("The nonce pool size is reached.") hasWarnedUser = false diff --git a/pkg/code/async/nonce/config.go b/pkg/code/async/nonce/config.go new file mode 100644 index 00000000..03e243f5 --- /dev/null +++ b/pkg/code/async/nonce/config.go @@ -0,0 +1,39 @@ +package async_nonce + +import ( + "github.com/code-payments/code-server/pkg/config" + "github.com/code-payments/code-server/pkg/config/env" +) + +const ( + envConfigPrefix = "NONCE_SERVICE_" + + cvmPublicKeyConfigEnvName = envConfigPrefix + "CVM_PUBLIC_KEY" + defaultCvmPublicKey = "invalid" // Ensure something valid is set + + solanaMainnetNoncePubkeyPrefixConfigEnvName = envConfigPrefix + "SOLANA_MAINNET_NONCE_PUBKEY_PREFIX" + defaultSolanaMainnetNoncePubkeyPrefix = "non" + + solanaMainnetNoncePoolSizeConfigEnvName = envConfigPrefix + "SOLANA_MAINNET_NONCE_POOL_SIZE" + defaultSolanaMainnetNoncePoolSize = 1000 +) + +type conf struct { + cvmPublicKey config.String + solanaMainnetNoncePubkeyPrefix config.String + solanaMainnetNoncePoolSize config.Uint64 +} + +// ConfigProvider defines how config values are pulled +type ConfigProvider func() *conf + +// WithEnvConfigs returns configuration pulled from environment variables +func WithEnvConfigs() ConfigProvider { + return func() *conf { + return &conf{ + cvmPublicKey: env.NewStringConfig(cvmPublicKeyConfigEnvName, defaultCvmPublicKey), + solanaMainnetNoncePubkeyPrefix: env.NewStringConfig(solanaMainnetNoncePubkeyPrefixConfigEnvName, defaultSolanaMainnetNoncePubkeyPrefix), + solanaMainnetNoncePoolSize: env.NewUint64Config(solanaMainnetNoncePoolSizeConfigEnvName, defaultSolanaMainnetNoncePoolSize), + } + } +} diff --git a/pkg/code/async/nonce/keys.go b/pkg/code/async/nonce/keys.go index 287b008d..fb2eb05d 100644 --- a/pkg/code/async/nonce/keys.go +++ b/pkg/code/async/nonce/keys.go @@ -17,7 +17,7 @@ func (p *service) generateKey(ctx context.Context) (*vault.Record, error) { // Perhaps this should be done outside this box. // Grind for a vanity key (slow) - key, err := vault.GrindKey(p.prefix) + key, err := vault.GrindKey(p.conf.solanaMainnetNoncePubkeyPrefix.Get(ctx)) if err != nil { return nil, err } @@ -49,7 +49,7 @@ func (p *service) generateKeys(ctx context.Context) error { return err } - reserveSize := (uint64(p.size) * 2) + reserveSize := (p.conf.solanaMainnetNoncePoolSize.Get(ctx) * 2) // If we have sufficient keys, don't generate any more. if res >= reserveSize { diff --git a/pkg/code/async/nonce/metrics.go b/pkg/code/async/nonce/metrics.go index aa5ce751..3ba4a33a 100644 --- a/pkg/code/async/nonce/metrics.go +++ b/pkg/code/async/nonce/metrics.go @@ -2,18 +2,20 @@ package async_nonce import ( "context" + "fmt" "time" - "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/code/data/nonce" + "github.com/code-payments/code-server/pkg/metrics" ) const ( - nonceCountMetricName = "Nonce/%s_count" nonceCountCheckEventName = "NonceCountPollingCheck" ) func (p *service) metricsGaugeWorker(ctx context.Context) error { + cvmPublicKey := p.conf.cvmPublicKey.Get(ctx) + delay := time.Second for { @@ -23,6 +25,7 @@ func (p *service) metricsGaugeWorker(ctx context.Context) error { case <-time.After(delay): start := time.Now() + // todo: optimize number of queries needed per polling check for _, useCase := range []nonce.Purpose{ nonce.PurposeClientTransaction, nonce.PurposeInternalServerProcess, @@ -35,12 +38,17 @@ func (p *service) metricsGaugeWorker(ctx context.Context) error { nonce.StateReserved, nonce.StateInvalid, } { - count, err := p.data.GetNonceCountByStateAndPurpose(ctx, state, useCase) + count, err := p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, useCase) if err != nil { continue } + recordNonceCountEvent(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, useCase, count) - recordNonceCountEvent(ctx, state, useCase, count) + count, err = p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentCvm, cvmPublicKey, state, useCase) + if err != nil { + continue + } + recordNonceCountEvent(ctx, nonce.EnvironmentCvm, cvmPublicKey, state, useCase, count) } } @@ -49,8 +57,9 @@ func (p *service) metricsGaugeWorker(ctx context.Context) error { } } -func recordNonceCountEvent(ctx context.Context, state nonce.State, useCase nonce.Purpose, count uint64) { +func recordNonceCountEvent(ctx context.Context, env nonce.Environment, instance string, state nonce.State, useCase nonce.Purpose, count uint64) { metrics.RecordEvent(ctx, nonceCountCheckEventName, map[string]interface{}{ + "pool": fmt.Sprintf("%s:%s", env.String(), instance), "use_case": useCase.String(), "state": state.String(), "count": count, diff --git a/pkg/code/async/nonce/pool.go b/pkg/code/async/nonce/pool.go index 11b83bd4..3a8c3195 100644 --- a/pkg/code/async/nonce/pool.go +++ b/pkg/code/async/nonce/pool.go @@ -5,8 +5,8 @@ import ( "sync" "time" - "github.com/mr-tron/base58/base58" "github.com/newrelic/go-agent/v3/newrelic" + "github.com/pkg/errors" "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/transaction" @@ -14,10 +14,15 @@ import ( "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/retry" "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/system" ) -func (p *service) worker(serviceCtx context.Context, state nonce.State, interval time.Duration) error { +// todo: We can generalize nonce handling by environment using an interface + +const ( + nonceBatchSize = 100 +) + +func (p *service) worker(serviceCtx context.Context, env nonce.Environment, instance string, state nonce.State, interval time.Duration) error { var cursor query.Cursor delay := interval @@ -33,6 +38,8 @@ func (p *service) worker(serviceCtx context.Context, state nonce.State, interval // Get a batch of nonce records in similar state (e.g. newly created, released, reserved, etc...) items, err := p.data.GetAllNonceByState( tracedCtx, + env, + instance, state, query.WithLimit(nonceBatchSize), query.WithCursor(cursor), @@ -80,7 +87,8 @@ func (p *service) handle(ctx context.Context, record *nonce.Record) error { States: StateUnknown: Newly created nonce on our side but the account might not - exist on Solana. + exist. Current implementation assumes we know the address + ahead of time. StateReleased: The account is confirmed to exist but we don't know its @@ -88,11 +96,11 @@ func (p *service) handle(ctx context.Context, record *nonce.Record) error { StateAvailable: Available to be used by a payment intent, subscription, or - other nonce-related transaction. + other nonce-related transaction/instruction. StateReserved: Reserved by a payment intent, subscription, or other - nonce-related transaction. + nonce-related transaction/instruction. StateInvalid: The nonce account is invalid (e.g. insufficient funds, etc). @@ -129,7 +137,12 @@ func (p *service) handle(ctx context.Context, record *nonce.Record) error { } func (p *service) handleUnknown(ctx context.Context, record *nonce.Record) error { - // Newly created nonces. + if record.Environment != nonce.EnvironmentSolana { + return errors.Errorf("%s environment not supported for %s state", record.Environment.String(), nonce.StateUnknown.String()) + } + + // Newly created nonces. Only supports Solana atm since we don't know the VDN + // address ahead of time. // We're going to the blockchain directly here (super slow btw) // because we don't capture the transaction through history yet (it only @@ -170,40 +183,49 @@ func (p *service) handleUnknown(ctx context.Context, record *nonce.Record) error } func (p *service) handleReleased(ctx context.Context, record *nonce.Record) error { + switch record.Environment { + case nonce.EnvironmentSolana, nonce.EnvironmentCvm: + default: + return errors.Errorf("%s environment not supported for %s state", record.Environment.String(), nonce.StateReleased.String()) + } + // Nonces that exist but we don't yet know their stored blockhash. - txn, err := p.getTransaction(ctx, record.Signature) - if err != nil { - return err + // Fetch the Solana transaction where the nonce would be consumed + var txn *transaction.Record + var err error + switch record.Environment { + case nonce.EnvironmentSolana: + txn, err = p.getTransaction(ctx, record.Signature) + if err != nil { + return err + } + case nonce.EnvironmentCvm: + return errors.New("todo: implement the process of getting submitted transaction from virtual instruction") } - // Sanity check the transaction is in a finalized or failed state + // Sanity check the Solana transaction is in a finalized or failed state if txn.ConfirmationState != transaction.ConfirmationFinalized && txn.ConfirmationState != transaction.ConfirmationFailed { return nil } - // Always get the account's state after the transaction's block to avoid - // having RPC nodes that are behind provide stale finalized data. - rawData, _, err := p.data.GetBlockchainAccountDataAfterBlock(ctx, record.Address, txn.Slot) - if err != nil { - return err - } - - if len(rawData) != system.NonceAccountSize { - // RPC call failed or something (maybe this node has no history?) - return ErrInvalidNonceAccountSize - } - - var data system.NonceAccount - err = data.Unmarshal(rawData) - if err != nil { - return err + // Get the next blockhash using a "safe" query + var nextBlockhash string + switch record.Environment { + case nonce.EnvironmentSolana: + nextBlockhash, err = p.getBlockhashFromSolanaNonce(ctx, record, txn.Slot) + if err != nil { + return err + } + case nonce.EnvironmentCvm: + nextBlockhash, err = p.getBlockhashFromCvmNonce(ctx, record, txn.Slot) + if err != nil { + return err + } } - nextBlockhash := base58.Encode(data.Blockhash) - - // Precautionary safety check, since it's an easy validation - if record.Blockhash == nextBlockhash { + // Precautionary safety checks, since it's an easy validation + if record.Blockhash == nextBlockhash || nextBlockhash == "" { return nil } diff --git a/pkg/code/async/nonce/service.go b/pkg/code/async/nonce/service.go index 4c62270e..ba4042d3 100644 --- a/pkg/code/async/nonce/service.go +++ b/pkg/code/async/nonce/service.go @@ -2,17 +2,16 @@ package async_nonce import ( "context" - "os" - "strconv" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/code-payments/code-server/pkg/code/data/nonce" + indexperpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" "github.com/code-payments/code-server/pkg/code/async" code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/code/data/nonce" ) var ( @@ -21,56 +20,31 @@ var ( ErrNoAvailableKeys = errors.New("no available keys in the vault") ) -const ( - nonceBatchSize = 100 - - noncePoolSizeDefault = 10 // Reserve is calculated as size * 2 - nonceKeyPrefixDefault = "non" - - nonceKeyPrefixEnv = "NONCE_PUBKEY_PREFIX" - noncePoolSizeEnv = "NONCE_POOL_SIZE" -) - type service struct { - log *logrus.Entry - data code_data.Provider + log *logrus.Entry + conf *conf + data code_data.Provider + vmIndexerClient indexperpb.IndexerClient - rent uint64 - prefix string - size int + rent uint64 } -func New(data code_data.Provider) async.Service { +func New(data code_data.Provider, vmIndexerClient indexperpb.IndexerClient, configProvider ConfigProvider) async.Service { return &service{ - log: logrus.StandardLogger().WithField("service", "nonce"), - data: data, - prefix: nonceKeyPrefixDefault, - size: noncePoolSizeDefault, + log: logrus.StandardLogger().WithField("service", "nonce"), + conf: configProvider(), + data: data, + vmIndexerClient: vmIndexerClient, } } func (p *service) Start(ctx context.Context, interval time.Duration) error { - // Look for user defined prefix value - prefix := os.Getenv(nonceKeyPrefixEnv) - if len(prefix) > 0 { - p.prefix = prefix - } - - // Look for user defined pool size value - sizeStr := os.Getenv(noncePoolSizeEnv) - if len(sizeStr) > 0 { - size, err := strconv.Atoi(sizeStr) - if err != nil { - return errors.Wrap(err, "invalid nonce pool size") - } - p.size = size - } - - // Generate vault keys until we have at least 10 in reserve to use for the pool + // Generate vault keys until we have a minimum in reserve to use for the pool + // on Solana mainnet go p.generateKeys(ctx) - // Watch the size of the nonce pool and create accounts if necessary - go p.generateNonceAccounts(ctx) + // Watch the size of the Solana mainnet nonce pool and create accounts if necessary + go p.generateNonceAccountsOnSolanaMainnet(ctx) // Setup workers to watch for nonce state changes on the Solana side for _, item := range []nonce.State{ @@ -79,7 +53,21 @@ func (p *service) Start(ctx context.Context, interval time.Duration) error { } { go func(state nonce.State) { - err := p.worker(ctx, state, interval) + err := p.worker(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, interval) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warnf("nonce processing loop terminated unexpectedly for state %d", state) + } + + }(item) + } + + // Setup workers to watch for nonce state changes on the CVM side + for _, item := range []nonce.State{ + nonce.StateReleased, + } { + go func(state nonce.State) { + + err := p.worker(ctx, nonce.EnvironmentCvm, p.conf.cvmPublicKey.Get(ctx), state, interval) if err != nil && err != context.Canceled { p.log.WithError(err).Warnf("nonce processing loop terminated unexpectedly for state %d", state) } diff --git a/pkg/code/async/nonce/util.go b/pkg/code/async/nonce/util.go index 428d57d1..bc6a7144 100644 --- a/pkg/code/async/nonce/util.go +++ b/pkg/code/async/nonce/util.go @@ -2,11 +2,13 @@ package async_nonce import ( "context" - "errors" "sync" "time" "github.com/mr-tron/base58/base58" + "github.com/pkg/errors" + + indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/nonce" @@ -218,3 +220,58 @@ func (p *service) broadcastTx(ctx context.Context, tx *solana.Transaction) { } } } + +func (p *service) getBlockhashFromSolanaNonce(ctx context.Context, record *nonce.Record, slot uint64) (string, error) { + if record.Environment != nonce.EnvironmentSolana { + return "", errors.Errorf("nonce environment is not %s", nonce.EnvironmentSolana.String()) + } + + // Always get the account's state after the transaction's block to avoid + // having RPC nodes that are behind provide stale finalized data. + rawData, _, err := p.data.GetBlockchainAccountDataAfterBlock(ctx, record.Address, slot) + if err != nil { + return "", err + } + + if len(rawData) != system.NonceAccountSize { + // RPC call failed or something (maybe this node has no history?) + return "", ErrInvalidNonceAccountSize + } + + var data system.NonceAccount + err = data.Unmarshal(rawData) + if err != nil { + return "", err + } + + return base58.Encode(data.Blockhash), nil +} + +func (p *service) getBlockhashFromCvmNonce(ctx context.Context, record *nonce.Record, slot uint64) (string, error) { + if record.Environment != nonce.EnvironmentCvm { + return "", errors.Errorf("nonce environment is not %s", nonce.EnvironmentCvm.String()) + } + + decodedVmAddress, err := base58.Decode(record.EnvironmentInstance) + if err != nil { + return "", err + } + + decodedVdnAddress, err := base58.Decode(record.Address) + if err != nil { + return "", err + } + + resp, err := p.vmIndexerClient.GetVirtualDurableNonce(ctx, &indexerpb.GetVirtualDurableNonceRequest{ + VmAccount: &indexerpb.Address{Value: decodedVmAddress}, + Address: &indexerpb.Address{Value: decodedVdnAddress}, + }) + if err != nil { + return "", err + } else if resp.Result != indexerpb.GetVirtualDurableNonceResponse_OK { + return "", errors.Errorf("received rpc result %s", resp.Result.String()) + } else if resp.Item.Slot <= slot { + return "", errors.New("rpc returned stale account state") + } + return base58.Encode(resp.Item.Account.Nonce.Value), nil +} diff --git a/pkg/code/async/sequencer/fulfillment_handler_test.go b/pkg/code/async/sequencer/fulfillment_handler_test.go index 235466b1..73ceade3 100644 --- a/pkg/code/async/sequencer/fulfillment_handler_test.go +++ b/pkg/code/async/sequencer/fulfillment_handler_test.go @@ -107,7 +107,7 @@ func TestInitializeLockedTimelockAccountFulfillmentHandler_OnDemandTransaction(t } env.generateAvailableNonce(t) - selectedNonce, err := transaction_util.SelectAvailableNonce(env.ctx, env.data, nonce.PurposeOnDemandTransaction) + selectedNonce, err := transaction_util.SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeOnDemandTransaction) require.NoError(t, err) assert.True(t, handler.SupportsOnDemandTransactions()) @@ -549,7 +549,7 @@ func TestTransferWithCommitmentFulfillmentHandler_OnDemandTransaction(t *testing handler := env.handlersByType[fulfillment.TransferWithCommitment] env.generateAvailableNonce(t) - selectedNonce, err := transaction_util.SelectAvailableNonce(env.ctx, env.data, nonce.PurposeOnDemandTransaction) + selectedNonce, err := transaction_util.SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeOnDemandTransaction) require.NoError(t, err) assert.True(t, handler.SupportsOnDemandTransactions()) @@ -1074,10 +1074,12 @@ func (e *fulfillmentHandlerTestEnv) setupForPayment(t *testing.T, fulfillmentRec Signature: *fulfillmentRecord.Signature, // We don't care about the below fields (yet) - Authority: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Blockhash: "bh", - Purpose: nonce.PurposeClientTransaction, - State: nonce.StateReserved, + Authority: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + Blockhash: "bh", + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeClientTransaction, + State: nonce.StateReserved, } require.NoError(t, e.data.SaveNonce(e.ctx, nonceRecord)) fulfillmentRecord.Nonce = pointer.String(nonceRecord.Address) @@ -1185,11 +1187,13 @@ func (e *fulfillmentHandlerTestEnv) generateAvailableNonce(t *testing.T) *nonce. CreatedAt: time.Now(), } nonceRecord := &nonce.Record{ - Address: nonceAccount.PublicKey().ToBase58(), - Authority: e.subsidizer.PublicKey().ToBase58(), - Blockhash: base58.Encode(bh[:]), - Purpose: nonce.PurposeOnDemandTransaction, - State: nonce.StateAvailable, + Address: nonceAccount.PublicKey().ToBase58(), + Authority: e.subsidizer.PublicKey().ToBase58(), + Blockhash: base58.Encode(bh[:]), + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeOnDemandTransaction, + State: nonce.StateAvailable, } require.NoError(t, e.data.SaveKey(e.ctx, nonceKey)) require.NoError(t, e.data.SaveNonce(e.ctx, nonceRecord)) diff --git a/pkg/code/async/sequencer/worker.go b/pkg/code/async/sequencer/worker.go index eda24ef0..ddf56cd7 100644 --- a/pkg/code/async/sequencer/worker.go +++ b/pkg/code/async/sequencer/worker.go @@ -10,14 +10,14 @@ import ( "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkg/errors" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/retry" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/transaction" transaction_util "github.com/code-payments/code-server/pkg/code/transaction" + "github.com/code-payments/code-server/pkg/database/query" + "github.com/code-payments/code-server/pkg/metrics" + "github.com/code-payments/code-server/pkg/pointer" + "github.com/code-payments/code-server/pkg/retry" ) func (p *service) worker(serviceCtx context.Context, state fulfillment.State, interval time.Duration) error { @@ -224,7 +224,7 @@ func (p *service) handlePending(ctx context.Context, record *fulfillment.Record) return errors.New("unexpected scheduled fulfillment without transaction data") } - selectedNonce, err := transaction_util.SelectAvailableNonce(ctx, p.data, nonce.PurposeOnDemandTransaction) + selectedNonce, err := transaction_util.SelectAvailableNonce(ctx, p.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeOnDemandTransaction) if err != nil { return err } diff --git a/pkg/code/async/sequencer/worker_test.go b/pkg/code/async/sequencer/worker_test.go index d8b23ca6..e2b4823f 100644 --- a/pkg/code/async/sequencer/worker_test.go +++ b/pkg/code/async/sequencer/worker_test.go @@ -10,11 +10,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/memo" - "github.com/code-payments/code-server/pkg/solana/system" - "github.com/code-payments/code-server/pkg/testutil" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/action" @@ -24,6 +19,11 @@ import ( "github.com/code-payments/code-server/pkg/code/data/transaction" "github.com/code-payments/code-server/pkg/code/data/vault" transaction_util "github.com/code-payments/code-server/pkg/code/transaction" + "github.com/code-payments/code-server/pkg/pointer" + "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/memo" + "github.com/code-payments/code-server/pkg/solana/system" + "github.com/code-payments/code-server/pkg/testutil" ) func TestFulfillmentWorker_StateUnknown_RemainInStateUnknown(t *testing.T) { @@ -294,12 +294,14 @@ func (e *workerTestEnv) createAnyFulfillmentInState(t *testing.T, state fulfillm require.NoError(t, e.data.PutAllActions(e.ctx, actionRecord)) nonceRecord := &nonce.Record{ - Address: *fulfillmentRecord.Nonce, - Blockhash: *fulfillmentRecord.Blockhash, - Authority: "code", - Purpose: nonce.PurposeClientTransaction, - Signature: *fulfillmentRecord.Signature, - State: nonce.StateReserved, + Address: *fulfillmentRecord.Nonce, + Blockhash: *fulfillmentRecord.Blockhash, + Authority: "code", + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeClientTransaction, + Signature: *fulfillmentRecord.Signature, + State: nonce.StateReserved, } require.NoError(t, e.data.SaveNonce(e.ctx, nonceRecord)) @@ -368,11 +370,13 @@ func (e *workerTestEnv) generateAvailableNonce(t *testing.T) *nonce.Record { CreatedAt: time.Now(), } nonceRecord := &nonce.Record{ - Address: nonceAccount.PublicKey().ToBase58(), - Authority: e.subsidizer.PublicKey().ToBase58(), - Blockhash: base58.Encode(bh[:]), - Purpose: nonce.PurposeOnDemandTransaction, - State: nonce.StateAvailable, + Address: nonceAccount.PublicKey().ToBase58(), + Authority: e.subsidizer.PublicKey().ToBase58(), + Blockhash: base58.Encode(bh[:]), + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeOnDemandTransaction, + State: nonce.StateAvailable, } require.NoError(t, e.data.SaveKey(e.ctx, nonceKey)) require.NoError(t, e.data.SaveNonce(e.ctx, nonceRecord)) diff --git a/pkg/code/async/treasury/recent_root.go b/pkg/code/async/treasury/recent_root.go index 9df41c7b..52b44879 100644 --- a/pkg/code/async/treasury/recent_root.go +++ b/pkg/code/async/treasury/recent_root.go @@ -9,10 +9,6 @@ import ( "github.com/mr-tron/base58" "github.com/sirupsen/logrus" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/fulfillment" @@ -22,6 +18,10 @@ import ( "github.com/code-payments/code-server/pkg/code/data/payment" "github.com/code-payments/code-server/pkg/code/data/treasury" "github.com/code-payments/code-server/pkg/code/transaction" + "github.com/code-payments/code-server/pkg/database/query" + "github.com/code-payments/code-server/pkg/pointer" + "github.com/code-payments/code-server/pkg/solana" + splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" ) // This method is expected to be extremely safe due to the implications of saving @@ -184,7 +184,7 @@ func (p *service) maybeSaveRecentRoot(ctx context.Context, treasuryPoolRecord *t State: action.StatePending, } - selectedNonce, err := transaction.SelectAvailableNonce(ctx, p.data, nonce.PurposeInternalServerProcess) + selectedNonce, err := transaction.SelectAvailableNonce(ctx, p.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) if err != nil { log.WithError(err).Warn("failure selecting available nonce") return err diff --git a/pkg/code/async/treasury/testutil.go b/pkg/code/async/treasury/testutil.go index ccd48671..4fb46b59 100644 --- a/pkg/code/async/treasury/testutil.go +++ b/pkg/code/async/treasury/testutil.go @@ -13,12 +13,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/solana/system" - "github.com/code-payments/code-server/pkg/testutil" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/action" @@ -31,6 +25,12 @@ import ( "github.com/code-payments/code-server/pkg/code/data/transaction" "github.com/code-payments/code-server/pkg/code/data/treasury" "github.com/code-payments/code-server/pkg/code/data/vault" + "github.com/code-payments/code-server/pkg/kin" + "github.com/code-payments/code-server/pkg/pointer" + "github.com/code-payments/code-server/pkg/solana" + splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" + "github.com/code-payments/code-server/pkg/solana/system" + "github.com/code-payments/code-server/pkg/testutil" ) type testEnv struct { @@ -436,11 +436,13 @@ func (e *testEnv) generateAvailableNonce(t *testing.T) *nonce.Record { CreatedAt: time.Now(), } nonceRecord := &nonce.Record{ - Address: nonceAccount.PublicKey().ToBase58(), - Authority: e.subsidizer.PublicKey().ToBase58(), - Blockhash: base58.Encode(bh[:]), - Purpose: nonce.PurposeInternalServerProcess, - State: nonce.StateAvailable, + Address: nonceAccount.PublicKey().ToBase58(), + Authority: e.subsidizer.PublicKey().ToBase58(), + Blockhash: base58.Encode(bh[:]), + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeInternalServerProcess, + State: nonce.StateAvailable, } require.NoError(t, e.data.SaveKey(e.ctx, nonceKey)) require.NoError(t, e.data.SaveNonce(e.ctx, nonceRecord)) diff --git a/pkg/code/common/subsidizer.go b/pkg/code/common/subsidizer.go index 903bfc60..595bfbe7 100644 --- a/pkg/code/common/subsidizer.go +++ b/pkg/code/common/subsidizer.go @@ -7,13 +7,15 @@ import ( "github.com/newrelic/go-agent/v3/newrelic" - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/solana" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/nonce" + "github.com/code-payments/code-server/pkg/metrics" + "github.com/code-payments/code-server/pkg/solana" ) +// todo: always assumes mainnet + const ( // Important Note: Be very careful changing this value, as it will completely // change timelock PDAs and have consequences with existing splitter treasuries. @@ -154,7 +156,7 @@ func EstimateUsedSubsidizerBalance(ctx context.Context, data code_data.Provider) fees += uint64(count) * lamportsConsumed } - numNoncesBeingCreated, err := data.GetNonceCountByState(ctx, nonce.StateUnknown) + numNoncesBeingCreated, err := data.GetNonceCountByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateUnknown) if err != nil { return 0, err } diff --git a/pkg/code/common/subsidizer_test.go b/pkg/code/common/subsidizer_test.go index f85e0bb5..878dfe1a 100644 --- a/pkg/code/common/subsidizer_test.go +++ b/pkg/code/common/subsidizer_test.go @@ -7,12 +7,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/pointer" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/nonce" + "github.com/code-payments/code-server/pkg/pointer" ) func TestEstimateUsedSubsidizerBalance(t *testing.T) { @@ -46,6 +46,8 @@ func TestEstimateUsedSubsidizerBalance(t *testing.T) { } for _, nonceRecord := range nonceRecords { nonceRecord.Authority = "code" + nonceRecord.Environment = nonce.EnvironmentSolana + nonceRecord.EnvironmentInstance = nonce.EnvironmentInstanceSolanaMainnet nonceRecord.Purpose = nonce.PurposeClientTransaction require.NoError(t, data.SaveNonce(ctx, nonceRecord)) } diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 49479c84..5403156e 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -175,11 +175,11 @@ type DatabaseData interface { // Nonce // -------------------------------------------------------------------------------- GetNonce(ctx context.Context, address string) (*nonce.Record, error) - GetNonceCount(ctx context.Context) (uint64, error) - GetNonceCountByState(ctx context.Context, state nonce.State) (uint64, error) - GetNonceCountByStateAndPurpose(ctx context.Context, state nonce.State, purpose nonce.Purpose) (uint64, error) - GetAllNonceByState(ctx context.Context, state nonce.State, opts ...query.Option) ([]*nonce.Record, error) - GetRandomAvailableNonceByPurpose(ctx context.Context, purpose nonce.Purpose) (*nonce.Record, error) + GetNonceCount(ctx context.Context, env nonce.Environment, instance string) (uint64, error) + GetNonceCountByState(ctx context.Context, env nonce.Environment, instance string, state nonce.State) (uint64, error) + GetNonceCountByStateAndPurpose(ctx context.Context, env nonce.Environment, instance string, state nonce.State, purpose nonce.Purpose) (uint64, error) + GetAllNonceByState(ctx context.Context, env nonce.Environment, instance string, state nonce.State, opts ...query.Option) ([]*nonce.Record, error) + GetRandomAvailableNonceByPurpose(ctx context.Context, env nonce.Environment, instance string, purpose nonce.Purpose) (*nonce.Record, error) SaveNonce(ctx context.Context, record *nonce.Record) error // Fulfillment @@ -723,25 +723,25 @@ func (dp *DatabaseProvider) SaveKey(ctx context.Context, record *vault.Record) e func (dp *DatabaseProvider) GetNonce(ctx context.Context, address string) (*nonce.Record, error) { return dp.nonces.Get(ctx, address) } -func (dp *DatabaseProvider) GetNonceCount(ctx context.Context) (uint64, error) { - return dp.nonces.Count(ctx) +func (dp *DatabaseProvider) GetNonceCount(ctx context.Context, env nonce.Environment, instance string) (uint64, error) { + return dp.nonces.Count(ctx, env, instance) } -func (dp *DatabaseProvider) GetNonceCountByState(ctx context.Context, state nonce.State) (uint64, error) { - return dp.nonces.CountByState(ctx, state) +func (dp *DatabaseProvider) GetNonceCountByState(ctx context.Context, env nonce.Environment, instance string, state nonce.State) (uint64, error) { + return dp.nonces.CountByState(ctx, env, instance, state) } -func (dp *DatabaseProvider) GetNonceCountByStateAndPurpose(ctx context.Context, state nonce.State, purpose nonce.Purpose) (uint64, error) { - return dp.nonces.CountByStateAndPurpose(ctx, state, purpose) +func (dp *DatabaseProvider) GetNonceCountByStateAndPurpose(ctx context.Context, env nonce.Environment, instance string, state nonce.State, purpose nonce.Purpose) (uint64, error) { + return dp.nonces.CountByStateAndPurpose(ctx, env, instance, state, purpose) } -func (dp *DatabaseProvider) GetAllNonceByState(ctx context.Context, state nonce.State, opts ...query.Option) ([]*nonce.Record, error) { +func (dp *DatabaseProvider) GetAllNonceByState(ctx context.Context, env nonce.Environment, instance string, state nonce.State, opts ...query.Option) ([]*nonce.Record, error) { req, err := query.DefaultPaginationHandler(opts...) if err != nil { return nil, err } - return dp.nonces.GetAllByState(ctx, state, req.Cursor, req.Limit, req.SortBy) + return dp.nonces.GetAllByState(ctx, env, instance, state, req.Cursor, req.Limit, req.SortBy) } -func (dp *DatabaseProvider) GetRandomAvailableNonceByPurpose(ctx context.Context, purpose nonce.Purpose) (*nonce.Record, error) { - return dp.nonces.GetRandomAvailableByPurpose(ctx, purpose) +func (dp *DatabaseProvider) GetRandomAvailableNonceByPurpose(ctx context.Context, env nonce.Environment, instance string, purpose nonce.Purpose) (*nonce.Record, error) { + return dp.nonces.GetRandomAvailableByPurpose(ctx, env, instance, purpose) } func (dp *DatabaseProvider) SaveNonce(ctx context.Context, record *nonce.Record) error { return dp.nonces.Save(ctx, record) diff --git a/pkg/code/data/nonce/memory/store.go b/pkg/code/data/nonce/memory/store.go index 5193689b..f89f877c 100644 --- a/pkg/code/data/nonce/memory/store.go +++ b/pkg/code/data/nonce/memory/store.go @@ -6,8 +6,8 @@ import ( "sort" "sync" - "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/code/data/nonce" + "github.com/code-payments/code-server/pkg/database/query" ) type store struct { @@ -57,9 +57,9 @@ func (s *store) findAddress(address string) *nonce.Record { return nil } -func (s *store) findByState(state nonce.State) []*nonce.Record { +func (s *store) findByState(env nonce.Environment, instance string, state nonce.State) []*nonce.Record { res := make([]*nonce.Record, 0) - for _, item := range s.records { + for _, item := range s.findByEnvironmentInstance(env, instance) { if item.State == state { res = append(res, item) } @@ -67,9 +67,9 @@ func (s *store) findByState(state nonce.State) []*nonce.Record { return res } -func (s *store) findByStateAndPurpose(state nonce.State, purpose nonce.Purpose) []*nonce.Record { +func (s *store) findByStateAndPurpose(env nonce.Environment, instance string, state nonce.State, purpose nonce.Purpose) []*nonce.Record { res := make([]*nonce.Record, 0) - for _, item := range s.records { + for _, item := range s.findByEnvironmentInstance(env, instance) { if item.State != state { continue } @@ -83,6 +83,22 @@ func (s *store) findByStateAndPurpose(state nonce.State, purpose nonce.Purpose) return res } +func (s *store) findByEnvironmentInstance(env nonce.Environment, instance string) []*nonce.Record { + res := make([]*nonce.Record, 0) + for _, item := range s.records { + if item.Environment != env { + continue + } + + if item.EnvironmentInstance != instance { + continue + } + + res = append(res, item) + } + return res +} + func (s *store) filter(items []*nonce.Record, cursor query.Cursor, limit uint64, direction query.Ordering) []*nonce.Record { var start uint64 @@ -115,26 +131,27 @@ func (s *store) filter(items []*nonce.Record, cursor query.Cursor, limit uint64, return res } -func (s *store) Count(ctx context.Context) (uint64, error) { +func (s *store) Count(ctx context.Context, env nonce.Environment, instance string) (uint64, error) { s.mu.Lock() defer s.mu.Unlock() - return uint64(len(s.records)), nil + res := s.findByEnvironmentInstance(env, instance) + return uint64(len(res)), nil } -func (s *store) CountByState(ctx context.Context, state nonce.State) (uint64, error) { +func (s *store) CountByState(ctx context.Context, env nonce.Environment, instance string, state nonce.State) (uint64, error) { s.mu.Lock() defer s.mu.Unlock() - res := s.findByState(state) + res := s.findByState(env, instance, state) return uint64(len(res)), nil } -func (s *store) CountByStateAndPurpose(ctx context.Context, state nonce.State, purpose nonce.Purpose) (uint64, error) { +func (s *store) CountByStateAndPurpose(ctx context.Context, env nonce.Environment, instance string, state nonce.State, purpose nonce.Purpose) (uint64, error) { s.mu.Lock() defer s.mu.Unlock() - res := s.findByStateAndPurpose(state, purpose) + res := s.findByStateAndPurpose(env, instance, state, purpose) return uint64(len(res)), nil } @@ -173,11 +190,11 @@ func (s *store) Get(ctx context.Context, address string) (*nonce.Record, error) return nil, nonce.ErrNonceNotFound } -func (s *store) GetAllByState(ctx context.Context, state nonce.State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*nonce.Record, error) { +func (s *store) GetAllByState(ctx context.Context, env nonce.Environment, instance string, state nonce.State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*nonce.Record, error) { s.mu.Lock() defer s.mu.Unlock() - if items := s.findByState(state); len(items) > 0 { + if items := s.findByState(env, instance, state); len(items) > 0 { res := s.filter(items, cursor, limit, direction) if len(res) == 0 { @@ -190,11 +207,11 @@ func (s *store) GetAllByState(ctx context.Context, state nonce.State, cursor que return nil, nonce.ErrNonceNotFound } -func (s *store) GetRandomAvailableByPurpose(ctx context.Context, purpose nonce.Purpose) (*nonce.Record, error) { +func (s *store) GetRandomAvailableByPurpose(ctx context.Context, env nonce.Environment, instance string, purpose nonce.Purpose) (*nonce.Record, error) { s.mu.Lock() defer s.mu.Unlock() - items := s.findByStateAndPurpose(nonce.StateAvailable, purpose) + items := s.findByStateAndPurpose(env, instance, nonce.StateAvailable, purpose) if len(items) == 0 { return nil, nonce.ErrNonceNotFound } diff --git a/pkg/code/data/nonce/nonce.go b/pkg/code/data/nonce/nonce.go index 69cec6d6..f50b1bca 100644 --- a/pkg/code/data/nonce/nonce.go +++ b/pkg/code/data/nonce/nonce.go @@ -7,6 +7,20 @@ import ( "github.com/mr-tron/base58" ) +type Environment uint8 + +const ( + EnvironmentUnknown Environment = iota + EnvironmentSolana // Environment instance is the cluster name (ie. mainnet, devnet, testnet, etc) + EnvironmentCvm // Environment instance is the VM public key +) + +const ( + EnvironmentInstanceSolanaMainnet = "mainnet" + EnvironmentInstanceSolanaDevnet = "devnet" + EnvironmentInstanceSolanaTestnet = "testnet" +) + var ( ErrNonceNotFound = errors.New("no records could be found") ErrInvalidNonce = errors.New("invalid nonce") @@ -17,8 +31,8 @@ type State uint8 const ( StateUnknown State = iota StateReleased // The nonce is almost ready but we don't know its blockhash yet. - StateAvailable // The nonce is available to be used by a payment intent, subscription, or other nonce-related transaction. - StateReserved // The nonce is reserved by a payment intent, subscription, or other nonce-related transaction. + StateAvailable // The nonce is available to be used by a payment intent, subscription, or other nonce-related transaction/instruction. + StateReserved // The nonce is reserved by a payment intent, subscription, or other nonce-related transaction/instruction. StateInvalid // The nonce account is invalid (e.g. insufficient funds, etc). ) @@ -43,8 +57,12 @@ type Record struct { Address string Authority string Blockhash string - Purpose Purpose - State State + + Environment Environment + EnvironmentInstance string + + Purpose Purpose + State State Signature string } @@ -55,13 +73,15 @@ func (r *Record) GetPublicKey() (ed25519.PublicKey, error) { func (r *Record) Clone() Record { return Record{ - Id: r.Id, - Address: r.Address, - Authority: r.Authority, - Blockhash: r.Blockhash, - Purpose: r.Purpose, - State: r.State, - Signature: r.Signature, + Id: r.Id, + Address: r.Address, + Authority: r.Authority, + Blockhash: r.Blockhash, + Environment: r.Environment, + EnvironmentInstance: r.EnvironmentInstance, + Purpose: r.Purpose, + State: r.State, + Signature: r.Signature, } } @@ -70,6 +90,8 @@ func (r *Record) CopyTo(dst *Record) { dst.Address = r.Address dst.Authority = r.Authority dst.Blockhash = r.Blockhash + dst.Environment = r.Environment + dst.EnvironmentInstance = r.EnvironmentInstance dst.Purpose = r.Purpose dst.State = r.State dst.Signature = r.Signature @@ -84,12 +106,33 @@ func (v *Record) Validate() error { return errors.New("authority address is required") } + if v.Environment == EnvironmentUnknown { + return errors.New("nonce environment must be set") + } + + if len(v.EnvironmentInstance) == 0 { + return errors.New("nonce environment instance must be set") + } + if v.Purpose == PurposeUnknown { return errors.New("nonce purpose must be set") } + return nil } +func (e Environment) String() string { + switch e { + case EnvironmentUnknown: + return "unknown" + case EnvironmentSolana: + return "solana" + case EnvironmentCvm: + return "cvm" + } + return "unknown" +} + func (s State) String() string { switch s { case StateUnknown: diff --git a/pkg/code/data/nonce/postgres/model.go b/pkg/code/data/nonce/postgres/model.go index b7a990b7..119e1e40 100644 --- a/pkg/code/data/nonce/postgres/model.go +++ b/pkg/code/data/nonce/postgres/model.go @@ -17,13 +17,15 @@ const ( ) type nonceModel struct { - Id sql.NullInt64 `db:"id"` - Address string `db:"address"` - Authority string `db:"authority"` - Blockhash string `db:"blockhash"` - Purpose uint `db:"purpose"` - State uint `db:"state"` - Signature string `db:"signature"` + Id sql.NullInt64 `db:"id"` + Address string `db:"address"` + Authority string `db:"authority"` + Blockhash string `db:"blockhash"` + Environment uint `db:"environment"` + EnvironmentInstance string `db:"environment_instance"` + Purpose uint `db:"purpose"` + State uint `db:"state"` + Signature string `db:"signature"` } func toNonceModel(obj *nonce.Record) (*nonceModel, error) { @@ -32,39 +34,43 @@ func toNonceModel(obj *nonce.Record) (*nonceModel, error) { } return &nonceModel{ - Id: sql.NullInt64{Int64: int64(obj.Id), Valid: true}, - Address: obj.Address, - Authority: obj.Authority, - Blockhash: obj.Blockhash, - Purpose: uint(obj.Purpose), - State: uint(obj.State), - Signature: obj.Signature, + Id: sql.NullInt64{Int64: int64(obj.Id), Valid: true}, + Address: obj.Address, + Authority: obj.Authority, + Blockhash: obj.Blockhash, + Environment: uint(obj.Environment), + EnvironmentInstance: obj.EnvironmentInstance, + Purpose: uint(obj.Purpose), + State: uint(obj.State), + Signature: obj.Signature, }, nil } func fromNonceModel(obj *nonceModel) *nonce.Record { return &nonce.Record{ - Id: uint64(obj.Id.Int64), - Address: obj.Address, - Authority: obj.Authority, - Blockhash: obj.Blockhash, - Purpose: nonce.Purpose(obj.Purpose), - State: nonce.State(obj.State), - Signature: obj.Signature, + Id: uint64(obj.Id.Int64), + Address: obj.Address, + Authority: obj.Authority, + Blockhash: obj.Blockhash, + Environment: nonce.Environment(obj.Environment), + EnvironmentInstance: obj.EnvironmentInstance, + Purpose: nonce.Purpose(obj.Purpose), + State: nonce.State(obj.State), + Signature: obj.Signature, } } func (m *nonceModel) dbSave(ctx context.Context, db *sqlx.DB) error { return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { query := `INSERT INTO ` + nonceTableName + ` - (address, authority, blockhash, purpose, state, signature) - VALUES ($1,$2,$3,$4,$5,$6) + (address, authority, blockhash, environment, environment_instance, purpose, state, signature) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (address) DO UPDATE - SET blockhash = $3, state = $5, signature = $6 + SET blockhash = $3, state = $7, signature = $8 WHERE ` + nonceTableName + `.address = $1 RETURNING - id, address, authority, blockhash, purpose, state, signature` + id, address, authority, blockhash, environment, environment_instance, purpose, state, signature` err := tx.QueryRowxContext( ctx, @@ -72,6 +78,8 @@ func (m *nonceModel) dbSave(ctx context.Context, db *sqlx.DB) error { m.Address, m.Authority, m.Blockhash, + m.Environment, + m.EnvironmentInstance, m.Purpose, m.State, m.Signature, @@ -81,11 +89,11 @@ func (m *nonceModel) dbSave(ctx context.Context, db *sqlx.DB) error { }) } -func dbGetCount(ctx context.Context, db *sqlx.DB) (uint64, error) { +func dbGetCount(ctx context.Context, db *sqlx.DB, env nonce.Environment, instance string) (uint64, error) { var res uint64 - query := `SELECT COUNT(*) FROM ` + nonceTableName - err := db.GetContext(ctx, &res, query) + query := `SELECT COUNT(*) FROM ` + nonceTableName + ` WHERE environment = $1 AND environment_instance = $2` + err := db.GetContext(ctx, &res, query, env, instance) if err != nil { return 0, err } @@ -93,11 +101,11 @@ func dbGetCount(ctx context.Context, db *sqlx.DB) (uint64, error) { return res, nil } -func dbGetCountByState(ctx context.Context, db *sqlx.DB, state nonce.State) (uint64, error) { +func dbGetCountByState(ctx context.Context, db *sqlx.DB, env nonce.Environment, instance string, state nonce.State) (uint64, error) { var res uint64 - query := `SELECT COUNT(*) FROM ` + nonceTableName + ` WHERE state = $1` - err := db.GetContext(ctx, &res, query, state) + query := `SELECT COUNT(*) FROM ` + nonceTableName + ` WHERE environment = $1 AND environment_instance = $2 AND state = $3` + err := db.GetContext(ctx, &res, query, env, instance, state) if err != nil { return 0, err } @@ -105,11 +113,11 @@ func dbGetCountByState(ctx context.Context, db *sqlx.DB, state nonce.State) (uin return res, nil } -func dbGetCountByStateAndPurpose(ctx context.Context, db *sqlx.DB, state nonce.State, purpose nonce.Purpose) (uint64, error) { +func dbGetCountByStateAndPurpose(ctx context.Context, db *sqlx.DB, env nonce.Environment, instance string, state nonce.State, purpose nonce.Purpose) (uint64, error) { var res uint64 - query := `SELECT COUNT(*) FROM ` + nonceTableName + ` WHERE state = $1 and purpose = $2` - err := db.GetContext(ctx, &res, query, state, purpose) + query := `SELECT COUNT(*) FROM ` + nonceTableName + ` WHERE environment = $1 AND environment_instance = $2 AND state = $3 and purpose = $4` + err := db.GetContext(ctx, &res, query, env, instance, state, purpose) if err != nil { return 0, err } @@ -121,7 +129,7 @@ func dbGetNonce(ctx context.Context, db *sqlx.DB, address string) (*nonceModel, res := &nonceModel{} query := `SELECT - id, address, authority, blockhash, purpose, state, signature + id, address, authority, blockhash, environment, environment_instance, purpose, state, signature FROM ` + nonceTableName + ` WHERE address = $1 ` @@ -133,7 +141,7 @@ func dbGetNonce(ctx context.Context, db *sqlx.DB, address string) (*nonceModel, return res, nil } -func dbGetAllByState(ctx context.Context, db *sqlx.DB, state nonce.State, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*nonceModel, error) { +func dbGetAllByState(ctx context.Context, db *sqlx.DB, env nonce.Environment, instance string, state nonce.State, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*nonceModel, error) { res := []*nonceModel{} // Signature null check is required because some legacy records didn't have this @@ -142,12 +150,12 @@ func dbGetAllByState(ctx context.Context, db *sqlx.DB, state nonce.State, cursor // // todo: Fix said nonce records query := `SELECT - id, address, authority, blockhash, purpose, state, signature + id, address, authority, blockhash, environment, environment_instance, purpose, state, signature FROM ` + nonceTableName + ` - WHERE (state = $1 AND signature IS NOT NULL) + WHERE environment = $1 AND environment_instance = $2 AND state = $3 AND signature IS NOT NULL ` - opts := []interface{}{state} + opts := []interface{}{env, instance, state} query, opts = q.PaginateQuery(query, opts, cursor, limit, direction) err := db.SelectContext(ctx, &res, query, opts...) @@ -166,7 +174,7 @@ func dbGetAllByState(ctx context.Context, db *sqlx.DB, state nonce.State, cursor // sufficiently efficient, as long as our nonce pool is larger than the max offset. // todo: We may need to tune the offset based on pool size and environment, but it // should be sufficiently good enough for now. -func dbGetRandomAvailableByPurpose(ctx context.Context, db *sqlx.DB, purpose nonce.Purpose) (*nonceModel, error) { +func dbGetRandomAvailableByPurpose(ctx context.Context, db *sqlx.DB, env nonce.Environment, instance string, purpose nonce.Purpose) (*nonceModel, error) { res := &nonceModel{} // Signature null check is required because some legacy records didn't have this @@ -175,20 +183,20 @@ func dbGetRandomAvailableByPurpose(ctx context.Context, db *sqlx.DB, purpose non // // todo: Fix said nonce records query := `SELECT - id, address, authority, blockhash, purpose, state, signature + id, address, authority, blockhash, environment, environment_instance, purpose, state, signature FROM ` + nonceTableName + ` - WHERE state = $1 AND purpose = $2 AND signature IS NOT NULL + WHERE environment = $1 AND environment_instance = $2 AND state = $3 AND purpose = $4 AND signature IS NOT NULL OFFSET FLOOR(RANDOM() * 100) LIMIT 1 ` fallbackQuery := `SELECT - id, address, authority, blockhash, purpose, state, signature + id, address, authority, blockhash, environment, environment_instance, purpose, state, signature FROM ` + nonceTableName + ` - WHERE state = $1 AND purpose = $2 AND signature IS NOT NULL + WHERE environment = $1 AND environment_instance = $2 AND state = $3 AND purpose = $4 AND signature IS NOT NULL LIMIT 1 ` - err := db.GetContext(ctx, res, query, nonce.StateAvailable, purpose) + err := db.GetContext(ctx, res, query, env, instance, nonce.StateAvailable, purpose) if err != nil { err = pgutil.CheckNoRows(err, nonce.ErrNonceNotFound) @@ -196,7 +204,7 @@ func dbGetRandomAvailableByPurpose(ctx context.Context, db *sqlx.DB, purpose non // strategy that will guarantee to select something if an available // nonce exists. if err == nonce.ErrNonceNotFound { - err := db.GetContext(ctx, res, fallbackQuery, nonce.StateAvailable, purpose) + err := db.GetContext(ctx, res, fallbackQuery, env, instance, nonce.StateAvailable, purpose) if err != nil { return nil, pgutil.CheckNoRows(err, nonce.ErrNonceNotFound) } diff --git a/pkg/code/data/nonce/postgres/store.go b/pkg/code/data/nonce/postgres/store.go index fa44d5fb..adf2b98e 100644 --- a/pkg/code/data/nonce/postgres/store.go +++ b/pkg/code/data/nonce/postgres/store.go @@ -4,8 +4,8 @@ import ( "context" "database/sql" - "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/code/data/nonce" + "github.com/code-payments/code-server/pkg/database/query" "github.com/jmoiron/sqlx" ) @@ -19,23 +19,18 @@ func New(db *sql.DB) nonce.Store { } } -// Count returns the total count of nonce accounts. -func (s *store) Count(ctx context.Context) (uint64, error) { - return dbGetCount(ctx, s.db) +func (s *store) Count(ctx context.Context, env nonce.Environment, instance string) (uint64, error) { + return dbGetCount(ctx, s.db, env, instance) } -// Count returns the total count of nonce accounts by state -func (s *store) CountByState(ctx context.Context, state nonce.State) (uint64, error) { - return dbGetCountByState(ctx, s.db, state) +func (s *store) CountByState(ctx context.Context, env nonce.Environment, instance string, state nonce.State) (uint64, error) { + return dbGetCountByState(ctx, s.db, env, instance, state) } -// CountByStateAndPurpose returns the total count of nonce accounts in the provided -// state and use case -func (s *store) CountByStateAndPurpose(ctx context.Context, state nonce.State, purpose nonce.Purpose) (uint64, error) { - return dbGetCountByStateAndPurpose(ctx, s.db, state, purpose) +func (s *store) CountByStateAndPurpose(ctx context.Context, env nonce.Environment, instance string, state nonce.State, purpose nonce.Purpose) (uint64, error) { + return dbGetCountByStateAndPurpose(ctx, s.db, env, instance, state, purpose) } -// Put saves nonce metadata to the store. func (s *store) Save(ctx context.Context, record *nonce.Record) error { obj, err := toNonceModel(record) if err != nil { @@ -53,9 +48,6 @@ func (s *store) Save(ctx context.Context, record *nonce.Record) error { return nil } -// Get finds the nonce record for a given address. -// -// Returns ErrNotFound if no record is found. func (s *store) Get(ctx context.Context, address string) (*nonce.Record, error) { obj, err := dbGetNonce(ctx, s.db, address) if err != nil { @@ -65,12 +57,8 @@ func (s *store) Get(ctx context.Context, address string) (*nonce.Record, error) return fromNonceModel(obj), nil } -// GetAllByState returns nonce records in the store for a given -// confirmation state. -// -// Returns ErrNotFound if no records are found. -func (s *store) GetAllByState(ctx context.Context, state nonce.State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*nonce.Record, error) { - models, err := dbGetAllByState(ctx, s.db, state, cursor, limit, direction) +func (s *store) GetAllByState(ctx context.Context, env nonce.Environment, instance string, state nonce.State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*nonce.Record, error) { + models, err := dbGetAllByState(ctx, s.db, env, instance, state, cursor, limit, direction) if err != nil { return nil, err } @@ -83,11 +71,8 @@ func (s *store) GetAllByState(ctx context.Context, state nonce.State, cursor que return nonces, nil } -// GetRandomAvailableByPurpose gets a random available nonce for a purpose. -// -// Returns ErrNotFound if no records are found. -func (s *store) GetRandomAvailableByPurpose(ctx context.Context, purpose nonce.Purpose) (*nonce.Record, error) { - model, err := dbGetRandomAvailableByPurpose(ctx, s.db, purpose) +func (s *store) GetRandomAvailableByPurpose(ctx context.Context, env nonce.Environment, instance string, purpose nonce.Purpose) (*nonce.Record, error) { + model, err := dbGetRandomAvailableByPurpose(ctx, s.db, env, instance, purpose) if err != nil { return nil, err } diff --git a/pkg/code/data/nonce/postgres/store_test.go b/pkg/code/data/nonce/postgres/store_test.go index bfc3b46f..fe1fb4a5 100644 --- a/pkg/code/data/nonce/postgres/store_test.go +++ b/pkg/code/data/nonce/postgres/store_test.go @@ -26,8 +26,12 @@ const ( authority text NOT NULL, blockhash text NULL, + environment integer NOT NULL, + environment_instance text NOT NULL, + purpose integer NOT NULL, state integer NOT NULL, + signature text NULL ); ` diff --git a/pkg/code/data/nonce/store.go b/pkg/code/data/nonce/store.go index 973feb37..4bc8e01c 100644 --- a/pkg/code/data/nonce/store.go +++ b/pkg/code/data/nonce/store.go @@ -7,15 +7,16 @@ import ( ) type Store interface { - // Count returns the total count of nonce accounts. - Count(ctx context.Context) (uint64, error) + // Count returns the total count of nonce accounts within an environment instance + Count(ctx context.Context, env Environment, instance string) (uint64, error) - // CountByState returns the total count of nonce accounts in the provided state. - CountByState(ctx context.Context, state State) (uint64, error) + // CountByState returns the total count of nonce accounts in the provided state within + // an environment instance + CountByState(ctx context.Context, env Environment, instance string, state State) (uint64, error) // CountByStateAndPurpose returns the total count of nonce accounts in the provided - // state and use case - CountByStateAndPurpose(ctx context.Context, state State, purpose Purpose) (uint64, error) + // state and use case within an environment instance + CountByStateAndPurpose(ctx context.Context, env Environment, instance string, state State, purpose Purpose) (uint64, error) // Save creates or updates nonce metadata in the store. Save(ctx context.Context, record *Record) error @@ -25,14 +26,15 @@ type Store interface { // Returns ErrNotFound if no record is found. Get(ctx context.Context, address string) (*Record, error) - // GetAllByState returns nonce records in the store for a given - // confirmation state. + // GetAllByState returns nonce records in the store for a given confirmation state + // within an environment intance. // // Returns ErrNotFound if no records are found. - GetAllByState(ctx context.Context, state State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) + GetAllByState(ctx context.Context, env Environment, instance string, state State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) - // GetRandomAvailableByPurpose gets a random available nonce for a purpose. + // GetRandomAvailableByPurpose gets a random available nonce for a purpose within + // an environment instance. // // Returns ErrNotFound if no records are found. - GetRandomAvailableByPurpose(ctx context.Context, purpose Purpose) (*Record, error) + GetRandomAvailableByPurpose(ctx context.Context, env Environment, instance string, purpose Purpose) (*Record, error) } diff --git a/pkg/code/data/nonce/tests/tests.go b/pkg/code/data/nonce/tests/tests.go index 8eee5cff..6d90a2d0 100644 --- a/pkg/code/data/nonce/tests/tests.go +++ b/pkg/code/data/nonce/tests/tests.go @@ -5,10 +5,11 @@ import ( "fmt" "testing" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/code-payments/code-server/pkg/code/data/nonce" + "github.com/code-payments/code-server/pkg/database/query" ) func RunTests(t *testing.T, s nonce.Store, teardown func()) { @@ -33,20 +34,27 @@ func testRoundTrip(t *testing.T, s nonce.Store) { assert.Nil(t, actual) expected := nonce.Record{ - Address: "test_address", - Authority: "test_authority", - Blockhash: "test_blockhash", - Purpose: nonce.PurposeClientTransaction, + Address: "test_address", + Authority: "test_authority", + Blockhash: "test_blockhash", + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeClientTransaction, + State: nonce.StateReserved, } + cloned := expected.Clone() err = s.Save(ctx, &expected) require.NoError(t, err) actual, err = s.Get(ctx, "test_address") require.NoError(t, err) - assert.Equal(t, expected.Address, actual.Address) - assert.Equal(t, expected.Authority, actual.Authority) - assert.Equal(t, expected.Blockhash, actual.Blockhash) - assert.Equal(t, expected.Purpose, actual.Purpose) + assert.Equal(t, cloned.Address, actual.Address) + assert.Equal(t, cloned.Authority, actual.Authority) + assert.Equal(t, cloned.Blockhash, actual.Blockhash) + assert.Equal(t, cloned.Environment, actual.Environment) + assert.Equal(t, cloned.EnvironmentInstance, actual.EnvironmentInstance) + assert.Equal(t, cloned.Purpose, actual.Purpose) + assert.Equal(t, cloned.State, actual.State) assert.EqualValues(t, 1, actual.Id) } @@ -54,10 +62,12 @@ func testUpdate(t *testing.T, s nonce.Store) { ctx := context.Background() expected := nonce.Record{ - Address: "test_address", - Authority: "test_authority", - Blockhash: "test_blockhash", - Purpose: nonce.PurposeInternalServerProcess, + Address: "test_address", + Authority: "test_authority", + Blockhash: "test_blockhash", + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeInternalServerProcess, } err := s.Save(ctx, &expected) require.NoError(t, err) @@ -93,6 +103,8 @@ func testGetAllByState(t *testing.T, s nonce.Store) { } for _, item := range expected { + item.Environment = nonce.EnvironmentSolana + item.EnvironmentInstance = nonce.EnvironmentInstanceSolanaMainnet item.Purpose = nonce.PurposeInternalServerProcess err := s.Save(ctx, &item) @@ -100,33 +112,33 @@ func testGetAllByState(t *testing.T, s nonce.Store) { } // Simple get all by state - actual, err := s.GetAllByState(ctx, nonce.StateReserved, query.EmptyCursor, 5, query.Ascending) + actual, err := s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.EmptyCursor, 5, query.Ascending) require.NoError(t, err) assert.Equal(t, 3, len(actual)) - actual, err = s.GetAllByState(ctx, nonce.StateUnknown, query.EmptyCursor, 5, query.Ascending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateUnknown, query.EmptyCursor, 5, query.Ascending) require.NoError(t, err) assert.Equal(t, 1, len(actual)) - actual, err = s.GetAllByState(ctx, nonce.StateInvalid, query.EmptyCursor, 5, query.Ascending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateInvalid, query.EmptyCursor, 5, query.Ascending) require.NoError(t, err) assert.Equal(t, 2, len(actual)) // Simple get all by state (reverse) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.EmptyCursor, 5, query.Descending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.EmptyCursor, 5, query.Descending) require.NoError(t, err) assert.Equal(t, 3, len(actual)) - actual, err = s.GetAllByState(ctx, nonce.StateUnknown, query.EmptyCursor, 5, query.Descending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateUnknown, query.EmptyCursor, 5, query.Descending) require.NoError(t, err) assert.Equal(t, 1, len(actual)) - actual, err = s.GetAllByState(ctx, nonce.StateInvalid, query.EmptyCursor, 5, query.Descending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateInvalid, query.EmptyCursor, 5, query.Descending) require.NoError(t, err) assert.Equal(t, 2, len(actual)) // Check items (asc) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.EmptyCursor, 5, query.Ascending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.EmptyCursor, 5, query.Ascending) require.NoError(t, err) assert.Equal(t, 3, len(actual)) assert.Equal(t, "t3", actual[0].Address) @@ -134,7 +146,7 @@ func testGetAllByState(t *testing.T, s nonce.Store) { assert.Equal(t, "t5", actual[2].Address) // Check items (desc) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.EmptyCursor, 5, query.Descending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.EmptyCursor, 5, query.Descending) require.NoError(t, err) assert.Equal(t, 3, len(actual)) assert.Equal(t, "t5", actual[0].Address) @@ -142,21 +154,21 @@ func testGetAllByState(t *testing.T, s nonce.Store) { assert.Equal(t, "t3", actual[2].Address) // Check items (asc + limit) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.EmptyCursor, 2, query.Ascending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.EmptyCursor, 2, query.Ascending) require.NoError(t, err) assert.Equal(t, 2, len(actual)) assert.Equal(t, "t3", actual[0].Address) assert.Equal(t, "t4", actual[1].Address) // Check items (desc + limit) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.EmptyCursor, 2, query.Descending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.EmptyCursor, 2, query.Descending) require.NoError(t, err) assert.Equal(t, 2, len(actual)) assert.Equal(t, "t5", actual[0].Address) assert.Equal(t, "t4", actual[1].Address) // Check items (asc + cursor) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.ToCursor(1), 5, query.Ascending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.ToCursor(1), 5, query.Ascending) require.NoError(t, err) assert.Equal(t, 3, len(actual)) assert.Equal(t, "t3", actual[0].Address) @@ -164,7 +176,7 @@ func testGetAllByState(t *testing.T, s nonce.Store) { assert.Equal(t, "t5", actual[2].Address) // Check items (desc + cursor) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.ToCursor(6), 5, query.Descending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.ToCursor(6), 5, query.Descending) require.NoError(t, err) assert.Equal(t, 3, len(actual)) assert.Equal(t, "t5", actual[0].Address) @@ -172,20 +184,20 @@ func testGetAllByState(t *testing.T, s nonce.Store) { assert.Equal(t, "t3", actual[2].Address) // Check items (asc + cursor) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.ToCursor(3), 5, query.Ascending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.ToCursor(3), 5, query.Ascending) require.NoError(t, err) assert.Equal(t, 2, len(actual)) assert.Equal(t, "t4", actual[0].Address) assert.Equal(t, "t5", actual[1].Address) // Check items (desc + cursor) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.ToCursor(4), 5, query.Descending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.ToCursor(4), 5, query.Descending) require.NoError(t, err) assert.Equal(t, 1, len(actual)) assert.Equal(t, "t3", actual[0].Address) // Check items (asc + cursor + limit) - actual, err = s.GetAllByState(ctx, nonce.StateReserved, query.ToCursor(3), 1, query.Ascending) + actual, err = s.GetAllByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.ToCursor(3), 1, query.Ascending) require.NoError(t, err) assert.Equal(t, 1, len(actual)) assert.Equal(t, "t4", actual[0].Address) @@ -204,7 +216,10 @@ func testGetCount(t *testing.T, s nonce.Store) { } for index, item := range expected { - count, err := s.Count(ctx) + item.Environment = nonce.EnvironmentSolana + item.EnvironmentInstance = nonce.EnvironmentInstanceSolanaMainnet + + count, err := s.Count(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet) require.NoError(t, err) assert.EqualValues(t, index, count) @@ -212,35 +227,35 @@ func testGetCount(t *testing.T, s nonce.Store) { require.NoError(t, err) } - count, err := s.CountByState(ctx, nonce.StateAvailable) + count, err := s.CountByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateAvailable) require.NoError(t, err) assert.EqualValues(t, 0, count) - count, err = s.CountByState(ctx, nonce.StateUnknown) + count, err = s.CountByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateUnknown) require.NoError(t, err) assert.EqualValues(t, 1, count) - count, err = s.CountByState(ctx, nonce.StateInvalid) + count, err = s.CountByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateInvalid) require.NoError(t, err) assert.EqualValues(t, 2, count) - count, err = s.CountByState(ctx, nonce.StateReserved) + count, err = s.CountByState(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved) require.NoError(t, err) assert.EqualValues(t, 3, count) - count, err = s.CountByStateAndPurpose(ctx, nonce.StateReserved, nonce.PurposeClientTransaction) + count, err = s.CountByStateAndPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, nonce.PurposeClientTransaction) require.NoError(t, err) assert.EqualValues(t, 2, count) - count, err = s.CountByStateAndPurpose(ctx, nonce.StateReserved, nonce.PurposeInternalServerProcess) + count, err = s.CountByStateAndPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, nonce.PurposeInternalServerProcess) require.NoError(t, err) assert.EqualValues(t, 1, count) - count, err = s.CountByStateAndPurpose(ctx, nonce.StateUnknown, nonce.PurposeClientTransaction) + count, err = s.CountByStateAndPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateUnknown, nonce.PurposeClientTransaction) require.NoError(t, err) assert.EqualValues(t, 1, count) - count, err = s.CountByStateAndPurpose(ctx, nonce.StateUnknown, nonce.PurposeInternalServerProcess) + count, err = s.CountByStateAndPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateUnknown, nonce.PurposeInternalServerProcess) require.NoError(t, err) assert.EqualValues(t, 0, count) } @@ -249,7 +264,7 @@ func testGetRandomAvailableByPurpose(t *testing.T, s nonce.Store) { t.Run("testGetRandomAvailableByPurpose", func(t *testing.T) { ctx := context.Background() - _, err := s.GetRandomAvailableByPurpose(ctx, nonce.PurposeClientTransaction) + _, err := s.GetRandomAvailableByPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) assert.Equal(t, nonce.ErrNonceNotFound, err) for _, purpose := range []nonce.Purpose{ @@ -263,12 +278,14 @@ func testGetRandomAvailableByPurpose(t *testing.T, s nonce.Store) { } { for i := 0; i < 500; i++ { record := &nonce.Record{ - Address: fmt.Sprintf("nonce_%s_%s_%d", purpose, state, i), - Authority: "authority", - Blockhash: "bh", - Purpose: purpose, - State: state, - Signature: "", + Address: fmt.Sprintf("nonce_%s_%s_%d", purpose, state, i), + Authority: "authority", + Blockhash: "bh", + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: purpose, + State: state, + Signature: "", } require.NoError(t, s.Save(ctx, record)) } @@ -277,7 +294,7 @@ func testGetRandomAvailableByPurpose(t *testing.T, s nonce.Store) { selectedByAddress := make(map[string]struct{}) for i := 0; i < 100; i++ { - actual, err := s.GetRandomAvailableByPurpose(ctx, nonce.PurposeClientTransaction) + actual, err := s.GetRandomAvailableByPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) require.NoError(t, err) assert.Equal(t, nonce.PurposeClientTransaction, actual.Purpose) assert.Equal(t, nonce.StateAvailable, actual.State) @@ -287,7 +304,7 @@ func testGetRandomAvailableByPurpose(t *testing.T, s nonce.Store) { selectedByAddress = make(map[string]struct{}) for i := 0; i < 100; i++ { - actual, err := s.GetRandomAvailableByPurpose(ctx, nonce.PurposeInternalServerProcess) + actual, err := s.GetRandomAvailableByPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) require.NoError(t, err) assert.Equal(t, nonce.PurposeInternalServerProcess, actual.Purpose) assert.Equal(t, nonce.StateAvailable, actual.State) diff --git a/pkg/code/server/grpc/transaction/v2/airdrop.go b/pkg/code/server/grpc/transaction/v2/airdrop.go index 122c4832..0eca26fb 100644 --- a/pkg/code/server/grpc/transaction/v2/airdrop.go +++ b/pkg/code/server/grpc/transaction/v2/airdrop.go @@ -438,7 +438,7 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner // Instead of constructing and validating everything manually, we could // have a proper client call SubmitIntent in a worker. - selectedNonce, err := transaction.SelectAvailableNonce(ctx, s.data, nonce.PurposeClientTransaction) + selectedNonce, err := transaction.SelectAvailableNonce(ctx, s.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) if err != nil { log.WithError(err).Warn("failure selecting available nonce") return nil, err diff --git a/pkg/code/server/grpc/transaction/v2/intent.go b/pkg/code/server/grpc/transaction/v2/intent.go index 9faffc47..f6d2c786 100644 --- a/pkg/code/server/grpc/transaction/v2/intent.go +++ b/pkg/code/server/grpc/transaction/v2/intent.go @@ -597,7 +597,7 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm var nonceAccount *common.Account var nonceBlockchash solana.Blockhash if createActionHandler.RequiresNonce(j) { - selectedNonce, err = transaction.SelectAvailableNonce(ctx, s.data, nonce.PurposeClientTransaction) + selectedNonce, err = transaction.SelectAvailableNonce(ctx, s.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) if err != nil { log.WithError(err).Warn("failure selecting available nonce") return handleSubmitIntentError(streamer, err) diff --git a/pkg/code/server/grpc/transaction/v2/testutil.go b/pkg/code/server/grpc/transaction/v2/testutil.go index 38c42626..3dbb5e88 100644 --- a/pkg/code/server/grpc/transaction/v2/testutil.go +++ b/pkg/code/server/grpc/transaction/v2/testutil.go @@ -499,11 +499,13 @@ func (s *serverTestEnv) generateAvailableNonce(t *testing.T) *nonce.Record { CreatedAt: time.Now(), } nonceRecord := &nonce.Record{ - Address: nonceAccount.PublicKey().ToBase58(), - Authority: s.subsidizer.PublicKey().ToBase58(), - Blockhash: base58.Encode(bh[:]), - Purpose: nonce.PurposeClientTransaction, - State: nonce.StateAvailable, + Address: nonceAccount.PublicKey().ToBase58(), + Authority: s.subsidizer.PublicKey().ToBase58(), + Blockhash: base58.Encode(bh[:]), + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeClientTransaction, + State: nonce.StateAvailable, } require.NoError(t, s.data.SaveKey(s.ctx, nonceKey)) require.NoError(t, s.data.SaveNonce(s.ctx, nonceRecord)) @@ -1195,7 +1197,7 @@ func (s serverTestEnv) assertAirdropped(t *testing.T, phone phoneTestEnv, airdro } func (s serverTestEnv) assertNoNoncesReserved(t *testing.T) { - count, err := s.data.GetNonceCountByState(s.ctx, nonce.StateReserved) + count, err := s.data.GetNonceCountByState(s.ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved) require.NoError(t, err) assert.EqualValues(t, 0, count) } @@ -1203,7 +1205,7 @@ func (s serverTestEnv) assertNoNoncesReserved(t *testing.T) { func (s serverTestEnv) assertNoNoncesReservedForIntent(t *testing.T, intentId string) { var cursor query.Cursor for { - nonceRecords, err := s.data.GetAllNonceByState(s.ctx, nonce.StateReserved, query.WithCursor(cursor)) + nonceRecords, err := s.data.GetAllNonceByState(s.ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.StateReserved, query.WithCursor(cursor)) if err == nonce.ErrNonceNotFound { return } diff --git a/pkg/code/transaction/nonce.go b/pkg/code/transaction/nonce.go index 1f6fc7f3..d5642432 100644 --- a/pkg/code/transaction/nonce.go +++ b/pkg/code/transaction/nonce.go @@ -8,12 +8,12 @@ import ( "github.com/mr-tron/base58" - "github.com/code-payments/code-server/pkg/retry" - "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/nonce" + "github.com/code-payments/code-server/pkg/retry" + "github.com/code-payments/code-server/pkg/solana" ) var ( @@ -51,11 +51,11 @@ type SelectedNonce struct { Blockhash solana.Blockhash } -// SelectAvailableNonce selects an available from the nonce pool for the specified -// use case. The returned nonce is marked as reserved without a signature, so it -// cannot be selected again. It's the responsibility of the external caller to make +// SelectAvailableNonce selects an available from the nonce pool within an environment +// for the specified use case. The returned nonce is marked as reserved without a signature, +// so it cannot be selected again. It's the responsibility of the external caller to make // it available again if it doesn't get assigned a fulfillment. -func SelectAvailableNonce(ctx context.Context, data code_data.Provider, useCase nonce.Purpose) (*SelectedNonce, error) { +func SelectAvailableNonce(ctx context.Context, data code_data.Provider, env nonce.Environment, instance string, useCase nonce.Purpose) (*SelectedNonce, error) { var lock *sync.Mutex var account *common.Account var bh solana.Blockhash @@ -65,7 +65,7 @@ func SelectAvailableNonce(ctx context.Context, data code_data.Provider, useCase globalNonceLock.Lock() defer globalNonceLock.Unlock() - randomRecord, err := data.GetRandomAvailableNonceByPurpose(ctx, useCase) + randomRecord, err := data.GetRandomAvailableNonceByPurpose(ctx, env, instance, useCase) if err == nonce.ErrNonceNotFound { return ErrNoAvailableNonces } else if err != nil { diff --git a/pkg/code/transaction/nonce_test.go b/pkg/code/transaction/nonce_test.go index ca8d7f28..f702e8bd 100644 --- a/pkg/code/transaction/nonce_test.go +++ b/pkg/code/transaction/nonce_test.go @@ -10,31 +10,31 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/testutil" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/vault" + "github.com/code-payments/code-server/pkg/pointer" + "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/testutil" ) func TestNonce_SelectAvailableNonce(t *testing.T) { env := setupNonceTestEnv(t) - allNonces := generateAvailableNonces(t, env, nonce.PurposeClientTransaction, 10) + allNonces := generateAvailableNonces(t, env, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction, 10) noncesByAddress := make(map[string]*nonce.Record) for _, nonceRecord := range allNonces { noncesByAddress[nonceRecord.Address] = nonceRecord } - _, err := SelectAvailableNonce(env.ctx, env.data, nonce.PurposeInternalServerProcess) + _, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) assert.Equal(t, ErrNoAvailableNonces, err) selectedNonces := make(map[string]struct{}) for i := 0; i < len(noncesByAddress); i++ { - selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.PurposeClientTransaction) + selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) require.NoError(t, err) _, ok := selectedNonces[selectedNonce.Account.PublicKey().ToBase58()] @@ -55,19 +55,22 @@ func TestNonce_SelectAvailableNonce(t *testing.T) { } assert.Len(t, selectedNonces, len(noncesByAddress)) - _, err = SelectAvailableNonce(env.ctx, env.data, nonce.PurposeClientTransaction) + _, err = SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) + assert.Equal(t, ErrNoAvailableNonces, err) + + _, err = SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) assert.Equal(t, ErrNoAvailableNonces, err) - _, err = SelectAvailableNonce(env.ctx, env.data, nonce.PurposeInternalServerProcess) + _, err = SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentCvm, testutil.NewRandomAccount(t).PublicKey().ToBase58(), nonce.PurposeClientTransaction) assert.Equal(t, ErrNoAvailableNonces, err) } func TestNonce_SelectNonceFromFulfillmentToUpgrade_HappyPath(t *testing.T) { env := setupNonceTestEnv(t) - generateAvailableNonces(t, env, nonce.PurposeClientTransaction, 2) + generateAvailableNonces(t, env, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction, 2) - selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.PurposeClientTransaction) + selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) require.NoError(t, err) fulfillmentToUpgrade := &fulfillment.Record{ @@ -102,9 +105,9 @@ func TestNonce_SelectNonceFromFulfillmentToUpgrade_HappyPath(t *testing.T) { func TestNonce_SelectNonceFromFulfillmentToUpgrade_DangerousPath(t *testing.T) { env := setupNonceTestEnv(t) - generateAvailableNonces(t, env, nonce.PurposeClientTransaction, 2) + generateAvailableNonces(t, env, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction, 2) - selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.PurposeClientTransaction) + selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) require.NoError(t, err) fulfillmentToUpgrade := &fulfillment.Record{ @@ -145,9 +148,9 @@ func TestNonce_SelectNonceFromFulfillmentToUpgrade_DangerousPath(t *testing.T) { func TestNonce_MarkReservedWithSignature(t *testing.T) { env := setupNonceTestEnv(t) - generateAvailableNonce(t, env, nonce.PurposeClientTransaction) + generateAvailableNonce(t, env, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) - selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.PurposeClientTransaction) + selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) require.NoError(t, err) assert.Error(t, selectedNonce.MarkReservedWithSignature(env.ctx, "")) @@ -164,9 +167,9 @@ func TestNonce_MarkReservedWithSignature(t *testing.T) { func TestNonce_UpdateSignature(t *testing.T) { env := setupNonceTestEnv(t) - generateAvailableNonce(t, env, nonce.PurposeClientTransaction) + generateAvailableNonce(t, env, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) - selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.PurposeClientTransaction) + selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) require.NoError(t, err) require.NoError(t, selectedNonce.MarkReservedWithSignature(env.ctx, "signature1")) @@ -183,9 +186,9 @@ func TestNonce_UpdateSignature(t *testing.T) { func TestNonce_ReleaseIfNotReserved(t *testing.T) { env := setupNonceTestEnv(t) - generateAvailableNonce(t, env, nonce.PurposeClientTransaction) + generateAvailableNonce(t, env, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) - selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.PurposeClientTransaction) + selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) require.NoError(t, err) require.NoError(t, selectedNonce.ReleaseIfNotReserved()) @@ -222,7 +225,7 @@ func setupNonceTestEnv(t *testing.T) nonceTestEnv { } } -func generateAvailableNonce(t *testing.T, env nonceTestEnv, useCase nonce.Purpose) *nonce.Record { +func generateAvailableNonce(t *testing.T, env nonceTestEnv, nonceEnv nonce.Environment, instance string, useCase nonce.Purpose) *nonce.Record { nonceAccount := testutil.NewRandomAccount(t) var bh solana.Blockhash @@ -235,21 +238,23 @@ func generateAvailableNonce(t *testing.T, env nonceTestEnv, useCase nonce.Purpos CreatedAt: time.Now(), } nonceRecord := &nonce.Record{ - Address: nonceAccount.PublicKey().ToBase58(), - Authority: common.GetSubsidizer().PublicKey().ToBase58(), - Blockhash: base58.Encode(bh[:]), - Purpose: nonce.PurposeClientTransaction, - State: nonce.StateAvailable, + Address: nonceAccount.PublicKey().ToBase58(), + Authority: common.GetSubsidizer().PublicKey().ToBase58(), + Blockhash: base58.Encode(bh[:]), + Environment: nonceEnv, + EnvironmentInstance: instance, + Purpose: nonce.PurposeClientTransaction, + State: nonce.StateAvailable, } require.NoError(t, env.data.SaveKey(env.ctx, nonceKey)) require.NoError(t, env.data.SaveNonce(env.ctx, nonceRecord)) return nonceRecord } -func generateAvailableNonces(t *testing.T, env nonceTestEnv, useCase nonce.Purpose, count int) []*nonce.Record { +func generateAvailableNonces(t *testing.T, env nonceTestEnv, nonceEnv nonce.Environment, instance string, useCase nonce.Purpose, count int) []*nonce.Record { var nonces []*nonce.Record for i := 0; i < count; i++ { - nonces = append(nonces, generateAvailableNonce(t, env, useCase)) + nonces = append(nonces, generateAvailableNonce(t, env, nonceEnv, instance, useCase)) } return nonces } From 1660e502e6ea9a528e087b2a91f81f33d83cef9d Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 24 Jul 2024 13:49:00 -0400 Subject: [PATCH 12/79] VM commitment worker (#155) * Update commitment store with changes related to VM * Fix treasury worker tests * Update commitment worker with new flows for the VM (currently missing closing) --- pkg/code/async/commitment/fulfillment.go | 25 -- pkg/code/async/commitment/metrics.go | 4 +- pkg/code/async/commitment/service.go | 1 - .../async/commitment/temporary_privacy.go | 2 +- .../commitment/temporary_privacy_test.go | 2 +- pkg/code/async/commitment/testutil.go | 251 +-------------- pkg/code/async/commitment/transaction.go | 256 --------------- pkg/code/async/commitment/util.go | 28 -- pkg/code/async/commitment/worker.go | 293 ++---------------- pkg/code/async/commitment/worker_test.go | 267 +--------------- pkg/code/async/treasury/testutil.go | 7 +- pkg/code/data/commitment/commitment.go | 53 +--- pkg/code/data/commitment/memory/store.go | 34 +- pkg/code/data/commitment/postgres/model.go | 101 ++---- pkg/code/data/commitment/postgres/store.go | 18 +- .../data/commitment/postgres/store_test.go | 10 +- pkg/code/data/commitment/store.go | 9 +- pkg/code/data/commitment/tests/tests.go | 105 ++----- pkg/code/data/internal.go | 10 +- pkg/solana/cvm/types_merkle_tree.go | 4 + 20 files changed, 130 insertions(+), 1350 deletions(-) delete mode 100644 pkg/code/async/commitment/fulfillment.go delete mode 100644 pkg/code/async/commitment/transaction.go diff --git a/pkg/code/async/commitment/fulfillment.go b/pkg/code/async/commitment/fulfillment.go deleted file mode 100644 index 1337e5fd..00000000 --- a/pkg/code/async/commitment/fulfillment.go +++ /dev/null @@ -1,25 +0,0 @@ -package async_commitment - -import ( - "context" - - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" -) - -func markFulfillmentAsActivelyScheduled(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) error { - if fulfillmentRecord.Id == 0 { - return nil - } - - if !fulfillmentRecord.DisableActiveScheduling { - return nil - } - - if fulfillmentRecord.State != fulfillment.StateUnknown { - return nil - } - - // Note: different than Save, since we don't have distributed locks - return data.MarkFulfillmentAsActivelyScheduled(ctx, fulfillmentRecord.Id) -} diff --git a/pkg/code/async/commitment/metrics.go b/pkg/code/async/commitment/metrics.go index 556f9be4..8ff70f0f 100644 --- a/pkg/code/async/commitment/metrics.go +++ b/pkg/code/async/commitment/metrics.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/code/data/commitment" + "github.com/code-payments/code-server/pkg/metrics" ) const ( @@ -26,8 +26,6 @@ func (p *service) metricsGaugeWorker(ctx context.Context) error { for _, state := range []commitment.State{ commitment.StateUnknown, commitment.StatePayingDestination, - commitment.StateReadyToOpen, - commitment.StateOpening, commitment.StateOpen, commitment.StateClosing, commitment.StateClosed, diff --git a/pkg/code/async/commitment/service.go b/pkg/code/async/commitment/service.go index ab324e10..abb7d7bf 100644 --- a/pkg/code/async/commitment/service.go +++ b/pkg/code/async/commitment/service.go @@ -27,7 +27,6 @@ func (p *service) Start(ctx context.Context, interval time.Duration) error { // Setup workers to watch for commitment state changes on the Solana side for _, item := range []commitment.State{ - commitment.StateReadyToOpen, commitment.StateOpen, commitment.StateClosed, diff --git a/pkg/code/async/commitment/temporary_privacy.go b/pkg/code/async/commitment/temporary_privacy.go index a952ec49..72f36078 100644 --- a/pkg/code/async/commitment/temporary_privacy.go +++ b/pkg/code/async/commitment/temporary_privacy.go @@ -20,7 +20,7 @@ var ( ) // GetDeadlineToUpgradePrivacy figures out at what point in time the temporary -// private transfer to the commitmetn vault should be played out. If no deadline +// private transfer to the commitment should be played out. If no deadline // exists, then ErrNoPrivacyUpgradeDeadline is returned. // // todo: move this someplace more common? diff --git a/pkg/code/async/commitment/temporary_privacy_test.go b/pkg/code/async/commitment/temporary_privacy_test.go index e70187fb..d2e5efc6 100644 --- a/pkg/code/async/commitment/temporary_privacy_test.go +++ b/pkg/code/async/commitment/temporary_privacy_test.go @@ -37,7 +37,7 @@ func TestGetDeadlineToUpgradePrivacy_HappyPath(t *testing.T) { commitmentRecord.TreasuryRepaid = false // Privacy is already upgraded - commitmentRecord.RepaymentDivertedTo = &commitmentRecords[1].Vault + commitmentRecord.RepaymentDivertedTo = &commitmentRecords[1].Address _, err = GetDeadlineToUpgradePrivacy(env.ctx, env.data, commitmentRecord) assert.Equal(t, ErrNoPrivacyUpgradeDeadline, err) commitmentRecord.RepaymentDivertedTo = nil diff --git a/pkg/code/async/commitment/testutil.go b/pkg/code/async/commitment/testutil.go index 8aa85366..c1d83388 100644 --- a/pkg/code/async/commitment/testutil.go +++ b/pkg/code/async/commitment/testutil.go @@ -2,13 +2,10 @@ package async_commitment import ( "context" - "crypto/ed25519" "encoding/hex" "fmt" - "math" "math/rand" "testing" - "time" "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" @@ -21,15 +18,12 @@ import ( "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/merkletree" - "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/code/data/vault" "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/solana/system" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/testutil" ) @@ -81,7 +75,7 @@ func setup(t *testing.T) testEnv { treasuryPool.Name, treasuryPool.MerkleTreeLevels, []merkletree.Seed{ - splitter_token.MerkleTreePrefix, + cvm.MerkleTreePrefix, treasuryPoolAddress.PublicKey().ToBytes(), }, false, @@ -110,15 +104,12 @@ func setup(t *testing.T) testEnv { func (e testEnv) simulateCommitment(t *testing.T, recentRoot string, state commitment.State) *commitment.Record { commitmentRecord := &commitment.Record{ - DataVersion: splitter_token.DataVersion1, - - Pool: e.treasuryPool.Address, Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Vault: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + Pool: e.treasuryPool.Address, RecentRoot: recentRoot, - Transcript: "transcript", + Transcript: "transcript", Destination: testutil.NewRandomAccount(t).PublicKey().ToBase58(), Amount: kin.ToQuarks(1), @@ -184,7 +175,7 @@ func (e testEnv) simulateCommitment(t *testing.T, recentRoot string, state commi Blockhash: pointer.String("bh"), Source: actionRecord.Source, - Destination: &commitmentRecord.Vault, + Destination: &e.treasuryPool.Vault, State: fulfillment.StateUnknown, } @@ -266,7 +257,7 @@ func (e testEnv) simulatePermanentPrivacyChequeCashed(t *testing.T, commitmentRe func (e testEnv) simulateCommitmentBeingUpgraded(t *testing.T, upgradeFrom, upgradeTo *commitment.Record) { require.Nil(t, upgradeFrom.RepaymentDivertedTo) - upgradeFrom.RepaymentDivertedTo = &upgradeTo.Vault + upgradeFrom.RepaymentDivertedTo = &upgradeTo.Address require.NoError(t, e.data.SaveCommitment(e.ctx, upgradeFrom)) fulfillmentRecords, err := e.data.GetAllFulfillmentsByTypeAndAction(e.ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, upgradeFrom.Intent, upgradeFrom.ActionId) @@ -278,204 +269,10 @@ func (e testEnv) simulateCommitmentBeingUpgraded(t *testing.T, upgradeFrom, upgr permanentPrivacyFulfillment.Id = 0 permanentPrivacyFulfillment.Signature = pointer.String(fmt.Sprintf("txn%d", rand.Uint64())) permanentPrivacyFulfillment.FulfillmentType = fulfillment.PermanentPrivacyTransferWithAuthority - permanentPrivacyFulfillment.Destination = &upgradeTo.Vault + permanentPrivacyFulfillment.Destination = &e.treasuryPool.Vault require.NoError(t, e.data.PutAllFulfillments(e.ctx, &permanentPrivacyFulfillment)) } -func (e testEnv) assertCommitmentVaultManagementFulfillmentsNotInjected(t *testing.T, commitmentRecord *commitment.Record) { - fulfillmentRecords, err := e.data.GetAllFulfillmentsByAction(e.ctx, commitmentRecord.Intent, commitmentRecord.ActionId) - require.NoError(t, err) - require.Len(t, fulfillmentRecords, 1) - assert.Equal(t, fulfillment.TemporaryPrivacyTransferWithAuthority, fulfillmentRecords[0].FulfillmentType) -} - -func (e testEnv) assertCommitmentVaultManagementFulfillmentsInjected(t *testing.T, commitmentRecord *commitment.Record) { - fulfillmentRecords, err := e.data.GetAllFulfillmentsByAction(e.ctx, commitmentRecord.Intent, commitmentRecord.ActionId) - require.NoError(t, err) - - require.Equal(t, fulfillment.TemporaryPrivacyTransferWithAuthority, fulfillmentRecords[0].FulfillmentType) - fulfillmentRecords = fulfillmentRecords[1:] - - require.Len(t, fulfillmentRecords, 6) - - poolAddressBytes, err := base58.Decode(e.treasuryPool.Address) - require.NoError(t, err) - - merkleRootBytes, err := hex.DecodeString(e.treasuryPool.GetMostRecentRoot()) - require.NoError(t, err) - - commitmentAddressBytes, err := base58.Decode(commitmentRecord.Address) - require.NoError(t, err) - - proofAddressBytes, proofBump, err := splitter_token.GetProofAddress(&splitter_token.GetProofAddressArgs{ - Pool: poolAddressBytes, - MerkleRoot: []byte(merkleRootBytes), - Commitment: commitmentAddressBytes, - }) - require.NoError(t, err) - - var uploadedProof []merkletree.Hash - for i, fulfillmentRecord := range fulfillmentRecords { - // - // Generic validation - // - - assert.Equal(t, commitmentRecord.Intent, fulfillmentRecord.Intent) - assert.Equal(t, intent.SendPrivatePayment, fulfillmentRecord.IntentType) - assert.EqualValues(t, commitmentRecord.ActionId, fulfillmentRecord.ActionId) - assert.Equal(t, action.PrivateTransfer, fulfillmentRecord.ActionType) - assert.Equal(t, commitmentRecord.Vault, fulfillmentRecord.Source) - assert.Nil(t, fulfillmentRecord.Destination) - assert.Nil(t, fulfillmentRecord.InitiatorPhoneNumber) - assert.Equal(t, fulfillment.StateUnknown, fulfillmentRecord.State) - - nonceRecord, err := e.data.GetNonce(e.ctx, *fulfillmentRecord.Nonce) - require.NoError(t, err) - assert.Equal(t, nonce.StateReserved, nonceRecord.State) - assert.Equal(t, *fulfillmentRecord.Signature, nonceRecord.Signature) - assert.Equal(t, nonceRecord.Blockhash, *fulfillmentRecord.Blockhash) - - var txn solana.Transaction - require.NoError(t, txn.Unmarshal(fulfillmentRecord.Data)) - - assert.Equal(t, *fulfillmentRecord.Blockhash, base58.Encode(txn.Message.RecentBlockhash[:])) - - expectedSignature := ed25519.Sign(e.subsidizer.PrivateKey().ToBytes(), txn.Message.Marshal()) - assert.Equal(t, base58.Encode(expectedSignature), *fulfillmentRecord.Signature) - assert.EqualValues(t, txn.Signatures[0][:], expectedSignature) - - require.NotEmpty(t, txn.Message.Instructions) - - advanceNonceIxn, err := system.DecompileAdvanceNonce(txn.Message, 0) - require.NoError(t, err) - - assert.Equal(t, *fulfillmentRecord.Nonce, base58.Encode(advanceNonceIxn.Nonce)) - assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), base58.Encode(advanceNonceIxn.Authority)) - - // - // Fulfillment-specific validation based on index - // - - var expectedFulfillmentType fulfillment.Type - var expectedIntentOrderingIndex uint64 - expectedFulfillmentOrderingIndex := uint32(i) - expectedDisableActiveScheduling := true - - switch i { - case 0: - expectedFulfillmentType = fulfillment.InitializeCommitmentProof - expectedDisableActiveScheduling = false - - require.Len(t, txn.Message.Instructions, 2) - - initializeIxnArgs, initializeIxnAccounts, err := splitter_token.InitializeProofInstructionFromLegacyInstruction(txn, 1) - require.NoError(t, err) - - assert.Equal(t, e.treasuryPool.Bump, initializeIxnArgs.PoolBump) - assert.EqualValues(t, merkleRootBytes, initializeIxnArgs.MerkleRoot) - assert.EqualValues(t, commitmentAddressBytes, initializeIxnArgs.Commitment) - - assert.Equal(t, e.treasuryPool.Address, base58.Encode(initializeIxnAccounts.Pool)) - assert.EqualValues(t, proofAddressBytes, initializeIxnAccounts.Proof) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), initializeIxnAccounts.Authority) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), initializeIxnAccounts.Payer) - case 1, 2, 3: - expectedFulfillmentType = fulfillment.UploadCommitmentProof - - require.Len(t, txn.Message.Instructions, 2) - - uploadIxnArgs, uploadIxnAccounts, err := splitter_token.UploadProofInstructionFromLegacyInstruction(txn, 1) - require.NoError(t, err) - - assert.Equal(t, e.treasuryPool.Bump, uploadIxnArgs.PoolBump) - assert.Equal(t, proofBump, uploadIxnArgs.ProofBump) - assert.EqualValues(t, len(uploadedProof), uploadIxnArgs.CurrentSize) - assert.True(t, uploadIxnArgs.DataSize >= 20) - assert.True(t, uploadIxnArgs.DataSize <= 22) - - for _, hash := range uploadIxnArgs.Data { - uploadedProof = append(uploadedProof, merkletree.Hash(hash)) - } - - assert.Equal(t, e.treasuryPool.Address, base58.Encode(uploadIxnAccounts.Pool)) - assert.EqualValues(t, proofAddressBytes, uploadIxnAccounts.Proof) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), uploadIxnAccounts.Authority) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), uploadIxnAccounts.Payer) - case 4: - expectedFulfillmentType = fulfillment.OpenCommitmentVault - - require.Len(t, txn.Message.Instructions, 3) - - verifyIxnArgs, verifyIxnAccounts, err := splitter_token.VerifyProofInstructionFromLegacyInstruction(txn, 1) - require.NoError(t, err) - - assert.Equal(t, e.treasuryPool.Bump, verifyIxnArgs.PoolBump) - assert.Equal(t, proofBump, verifyIxnArgs.ProofBump) - - assert.Equal(t, e.treasuryPool.Address, base58.Encode(verifyIxnAccounts.Pool)) - assert.EqualValues(t, proofAddressBytes, verifyIxnAccounts.Proof) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), verifyIxnAccounts.Authority) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), verifyIxnAccounts.Payer) - - openIxnArgs, openIxnAccounts, err := splitter_token.OpenTokenAccountInstructionFromLegacyInstruction(txn, 2) - require.NoError(t, err) - - assert.Equal(t, e.treasuryPool.Bump, openIxnArgs.PoolBump) - assert.Equal(t, proofBump, openIxnArgs.ProofBump) - - assert.Equal(t, e.treasuryPool.Address, base58.Encode(openIxnAccounts.Pool)) - assert.EqualValues(t, proofAddressBytes, openIxnAccounts.Proof) - assert.Equal(t, commitmentRecord.Vault, base58.Encode(openIxnAccounts.CommitmentVault)) - assert.EqualValues(t, kin.TokenMint, openIxnAccounts.Mint) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), openIxnAccounts.Authority) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), openIxnAccounts.Payer) - case 5: - expectedFulfillmentType = fulfillment.CloseCommitmentVault - - expectedIntentOrderingIndex = uint64(math.MaxInt64) - expectedFulfillmentOrderingIndex = 0 - - require.Len(t, txn.Message.Instructions, 3) - - closeTokenIxnArgs, closeTokenIxnAccounts, err := splitter_token.CloseTokenAccountInstructionFromLegacyInstruction(txn, 1) - require.NoError(t, err) - - assert.Equal(t, e.treasuryPool.Bump, closeTokenIxnArgs.PoolBump) - assert.Equal(t, proofBump, closeTokenIxnArgs.ProofBump) - assert.Equal(t, commitmentRecord.VaultBump, closeTokenIxnArgs.VaultBump) - - assert.Equal(t, e.treasuryPool.Address, base58.Encode(closeTokenIxnAccounts.Pool)) - assert.EqualValues(t, proofAddressBytes, closeTokenIxnAccounts.Proof) - assert.Equal(t, commitmentRecord.Vault, base58.Encode(closeTokenIxnAccounts.CommitmentVault)) - assert.Equal(t, e.treasuryPool.Vault, base58.Encode(closeTokenIxnAccounts.PoolVault)) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), closeTokenIxnAccounts.Authority) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), closeTokenIxnAccounts.Payer) - - closeProofIxnArgs, closeProofIxnAccounts, err := splitter_token.CloseProofInstructionFromLegacyInstruction(txn, 2) - require.NoError(t, err) - - assert.Equal(t, e.treasuryPool.Bump, closeProofIxnArgs.PoolBump) - assert.Equal(t, proofBump, closeProofIxnArgs.ProofBump) - - assert.Equal(t, e.treasuryPool.Address, base58.Encode(closeProofIxnAccounts.Pool)) - assert.EqualValues(t, proofAddressBytes, closeProofIxnAccounts.Proof) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), closeProofIxnAccounts.Authority) - assert.EqualValues(t, e.subsidizer.PublicKey().ToBytes(), closeProofIxnAccounts.Payer) - default: - assert.Fail(t, "too many fulfillments") - } - - assert.Equal(t, expectedFulfillmentType, fulfillmentRecord.FulfillmentType) - assert.Equal(t, expectedIntentOrderingIndex, fulfillmentRecord.IntentOrderingIndex) - assert.EqualValues(t, 0, fulfillmentRecord.ActionOrderingIndex) - assert.Equal(t, expectedFulfillmentOrderingIndex, fulfillmentRecord.FulfillmentOrderingIndex) - assert.Equal(t, expectedDisableActiveScheduling, fulfillmentRecord.DisableActiveScheduling) - } - - require.Len(t, uploadedProof, int(e.treasuryPool.MerkleTreeLevels)) - assert.True(t, merkletree.Verify(uploadedProof, merkleRootBytes, commitmentAddressBytes)) -} - func (e testEnv) assertCommitmentState(t *testing.T, address string, expected commitment.State) { commitmentRecord, err := e.data.GetCommitmentByAddress(e.ctx, address) require.NoError(t, err) @@ -487,37 +284,3 @@ func (e testEnv) assertCommitmentRepaymentStatus(t *testing.T, address string, e require.NoError(t, err) assert.Equal(t, expected, commitmentRecord.TreasuryRepaid) } - -func (e *testEnv) generateAvailableNonce(t *testing.T) *nonce.Record { - nonceAccount := testutil.NewRandomAccount(t) - - var bh solana.Blockhash - rand.Read(bh[:]) - - nonceKey := &vault.Record{ - PublicKey: nonceAccount.PublicKey().ToBase58(), - PrivateKey: nonceAccount.PrivateKey().ToBase58(), - State: vault.StateAvailable, - CreatedAt: time.Now(), - } - nonceRecord := &nonce.Record{ - Address: nonceAccount.PublicKey().ToBase58(), - Authority: e.subsidizer.PublicKey().ToBase58(), - Blockhash: base58.Encode(bh[:]), - Environment: nonce.EnvironmentSolana, - EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, - Purpose: nonce.PurposeInternalServerProcess, - State: nonce.StateAvailable, - } - require.NoError(t, e.data.SaveKey(e.ctx, nonceKey)) - require.NoError(t, e.data.SaveNonce(e.ctx, nonceRecord)) - return nonceRecord -} - -func (e *testEnv) generateAvailableNonces(t *testing.T, count int) []*nonce.Record { - var nonces []*nonce.Record - for i := 0; i < count; i++ { - nonces = append(nonces, e.generateAvailableNonce(t)) - } - return nonces -} diff --git a/pkg/code/async/commitment/transaction.go b/pkg/code/async/commitment/transaction.go deleted file mode 100644 index fe2870de..00000000 --- a/pkg/code/async/commitment/transaction.go +++ /dev/null @@ -1,256 +0,0 @@ -package async_commitment - -import ( - "context" - "crypto/ed25519" - "encoding/hex" - "errors" - - "github.com/mr-tron/base58" - - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/solana" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/merkletree" -) - -type commitmentManagementAccounts struct { - Commitment ed25519.PublicKey - CommitmentVault ed25519.PublicKey - - Pool ed25519.PublicKey - PoolVault ed25519.PublicKey - - Proof ed25519.PublicKey -} - -type commitmentManagementArgs struct { - CommitmentVaultBump uint8 - PoolBump uint8 - ProofBump uint8 - - MerkleRoot merkletree.Hash - MerkleProof []merkletree.Hash -} - -func (p *service) getCommitmentManagementTxnAccountsAndArgs( - ctx context.Context, - commitmentRecord *commitment.Record, -) (*commitmentManagementAccounts, *commitmentManagementArgs, error) { - treasuryPoolRecord, err := p.data.GetTreasuryPoolByAddress(ctx, commitmentRecord.Pool) - if err != nil { - return nil, nil, err - } - - poolAddressBytes, err := base58.Decode(treasuryPoolRecord.Address) - if err != nil { - return nil, nil, err - } - - poolVaultAddressBytes, err := base58.Decode(treasuryPoolRecord.Vault) - if err != nil { - return nil, nil, err - } - - commitmentAddressBytes, err := base58.Decode(commitmentRecord.Address) - if err != nil { - return nil, nil, err - } - - commitmentVaultAddressBytes, err := base58.Decode(commitmentRecord.Vault) - if err != nil { - return nil, nil, err - } - - merkleTree, err := p.data.LoadExistingMerkleTree(ctx, treasuryPoolRecord.Name, true) - if err != nil { - return nil, nil, err - } - - forLeaf, err := merkleTree.GetLeafNode(ctx, commitmentAddressBytes) - if err != nil { - return nil, nil, err - } - - var merkleRootBytes merkletree.Hash - var untilLeaf *merkletree.Node - for _, recentRoot := range []string{ - treasuryPoolRecord.GetMostRecentRoot(), - // Guaranteed to be in the tree in the event the most recent root isn't there, - // unless it's a brand new tree. - treasuryPoolRecord.GetPreviousMostRecentRoot(), - } { - merkleRootBytes, err = hex.DecodeString(recentRoot) - if err != nil { - return nil, nil, err - } - - untilLeaf, err = merkleTree.GetLeafNodeForRoot(ctx, merkleRootBytes) - if err != nil && err != merkletree.ErrLeafNotFound && err != merkletree.ErrRootNotFound { - return nil, nil, err - } - - if untilLeaf != nil { - break - } - } - - if untilLeaf == nil || forLeaf.Index > untilLeaf.Index { - return nil, nil, errors.New("proof not available") - } - - proof, err := merkleTree.GetProofForLeafAtIndex(ctx, forLeaf.Index, untilLeaf.Index) - if err != nil { - return nil, nil, err - } - - // Keeping this in as a sanity check, for now. If we can't validate the proof, - // then the splitter program won't either. - if !merkletree.Verify(proof, merkleRootBytes, commitmentAddressBytes) { - return nil, nil, errors.New("generated an invalid proof") - } - - proofAddressBytes, proofBump, err := splitter_token.GetProofAddress(&splitter_token.GetProofAddressArgs{ - Pool: poolAddressBytes, - MerkleRoot: []byte(merkleRootBytes), - Commitment: commitmentAddressBytes, - }) - if err != nil { - return nil, nil, err - } - - accounts := &commitmentManagementAccounts{ - Commitment: commitmentAddressBytes, - CommitmentVault: commitmentVaultAddressBytes, - - Pool: poolAddressBytes, - PoolVault: poolVaultAddressBytes, - - Proof: proofAddressBytes, - } - - args := &commitmentManagementArgs{ - CommitmentVaultBump: commitmentRecord.VaultBump, - PoolBump: treasuryPoolRecord.Bump, - ProofBump: proofBump, - - MerkleRoot: merkleRootBytes, - MerkleProof: proof, - } - - return accounts, args, nil -} - -func makeInitializeProofInstructions(accounts *commitmentManagementAccounts, args *commitmentManagementArgs) []solana.Instruction { - initializeProofInstruction := splitter_token.NewInitializeProofInstruction( - &splitter_token.InitializeProofInstructionAccounts{ - Pool: accounts.Pool, - Proof: accounts.Proof, - Authority: common.GetSubsidizer().PublicKey().ToBytes(), - Payer: common.GetSubsidizer().PublicKey().ToBytes(), - }, - &splitter_token.InitializeProofInstructionArgs{ - PoolBump: args.PoolBump, - MerkleRoot: splitter_token.Hash(args.MerkleRoot), - Commitment: accounts.Commitment, - }, - ).ToLegacyInstruction() - - return []solana.Instruction{initializeProofInstruction} -} - -func makeUploadPartialProofInstructions(accounts *commitmentManagementAccounts, args *commitmentManagementArgs, fromChunkInclusive, toChunkInclusive int) []solana.Instruction { - var partialProof []splitter_token.Hash - for i := fromChunkInclusive; i < toChunkInclusive+1; i++ { - partialProof = append(partialProof, splitter_token.Hash(args.MerkleProof[i])) - } - - uploadProofInstruction := splitter_token.NewUploadProofInstruction( - &splitter_token.UploadProofInstructionAccounts{ - Pool: accounts.Pool, - Proof: accounts.Proof, - Authority: common.GetSubsidizer().PublicKey().ToBytes(), - Payer: common.GetSubsidizer().PublicKey().ToBytes(), - }, - &splitter_token.UploadProofInstructionArgs{ - PoolBump: args.PoolBump, - ProofBump: args.ProofBump, - CurrentSize: uint8(fromChunkInclusive), - DataSize: uint8(len(partialProof)), - Data: partialProof, - }, - ).ToLegacyInstruction() - - return []solana.Instruction{uploadProofInstruction} -} - -func makeVerifyProofInstructions(accounts *commitmentManagementAccounts, args *commitmentManagementArgs) []solana.Instruction { - verifyProofInstruction := splitter_token.NewVerifyProofInstruction( - &splitter_token.VerifyProofInstructionAccounts{ - Pool: accounts.Pool, - Proof: accounts.Proof, - Authority: common.GetSubsidizer().PublicKey().ToBytes(), - Payer: common.GetSubsidizer().PublicKey().ToBytes(), - }, - &splitter_token.VerifyProofInstructionArgs{ - PoolBump: args.PoolBump, - ProofBump: args.ProofBump, - }, - ).ToLegacyInstruction() - - return []solana.Instruction{verifyProofInstruction} -} - -func makeOpenCommitmentVaultInstructions(accounts *commitmentManagementAccounts, args *commitmentManagementArgs) []solana.Instruction { - openInstruction := splitter_token.NewOpenTokenAccountInstruction( - &splitter_token.OpenTokenAccountInstructionAccounts{ - Pool: accounts.Pool, - Proof: accounts.Proof, - CommitmentVault: accounts.CommitmentVault, - Mint: kin.TokenMint, - Authority: common.GetSubsidizer().PublicKey().ToBytes(), - Payer: common.GetSubsidizer().PublicKey().ToBytes(), - }, - &splitter_token.OpenTokenAccountInstructionArgs{ - PoolBump: args.PoolBump, - ProofBump: args.ProofBump, - }, - ).ToLegacyInstruction() - - return []solana.Instruction{openInstruction} -} - -func makeCloseCommitmentVaultInstructions(accounts *commitmentManagementAccounts, args *commitmentManagementArgs) []solana.Instruction { - closeVaultInstruction := splitter_token.NewCloseTokenAccountInstruction( - &splitter_token.CloseTokenAccountInstructionAccounts{ - Pool: accounts.Pool, - Proof: accounts.Proof, - CommitmentVault: accounts.CommitmentVault, - PoolVault: accounts.PoolVault, - Authority: common.GetSubsidizer().PublicKey().ToBytes(), - Payer: common.GetSubsidizer().PublicKey().ToBytes(), - }, - &splitter_token.CloseTokenAccountInstructionArgs{ - PoolBump: args.PoolBump, - ProofBump: args.ProofBump, - VaultBump: args.CommitmentVaultBump, - }, - ).ToLegacyInstruction() - - closeProofInstruction := splitter_token.NewCloseProofInstruction( - &splitter_token.CloseProofInstructionAccounts{ - Pool: accounts.Pool, - Proof: accounts.Proof, - Authority: common.GetSubsidizer().PublicKey().ToBytes(), - Payer: common.GetSubsidizer().PublicKey().ToBytes(), - }, - &splitter_token.CloseProofInstructionArgs{ - PoolBump: args.PoolBump, - ProofBump: args.ProofBump, - }, - ).ToLegacyInstruction() - - return []solana.Instruction{closeVaultInstruction, closeProofInstruction} -} diff --git a/pkg/code/async/commitment/util.go b/pkg/code/async/commitment/util.go index 5b016be8..3d68b763 100644 --- a/pkg/code/async/commitment/util.go +++ b/pkg/code/async/commitment/util.go @@ -6,29 +6,10 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" ) // Every other state is currently managed after successful fulfillment submission -func markCommitmentAsOpening(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return err - } - - if commitmentRecord.State == commitment.StateOpening { - return nil - } - - if commitmentRecord.State != commitment.StateReadyToOpen { - return errors.New("commitment in invalid state") - } - - commitmentRecord.State = commitment.StateOpening - return data.SaveCommitment(ctx, commitmentRecord) -} - func markCommitmentAsClosing(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) if err != nil { @@ -43,15 +24,6 @@ func markCommitmentAsClosing(ctx context.Context, data code_data.Provider, inten return errors.New("commitment in invalid state") } - fulfillmentRecords, err := data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.CloseCommitmentVault, intentId, actionId) - if err != nil { - return err - } - err = markFulfillmentAsActivelyScheduled(ctx, data, fulfillmentRecords[0]) - if err != nil { - return err - } - commitmentRecord.State = commitment.StateClosing return data.SaveCommitment(ctx, commitmentRecord) } diff --git a/pkg/code/async/commitment/worker.go b/pkg/code/async/commitment/worker.go index 75318b19..80652c28 100644 --- a/pkg/code/async/commitment/worker.go +++ b/pkg/code/async/commitment/worker.go @@ -2,28 +2,20 @@ package async_commitment import ( "context" - "database/sql" "errors" - "math" "sync" "time" "github.com/mr-tron/base58" "github.com/newrelic/go-agent/v3/newrelic" - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/merkletree" - "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/retry" - "github.com/code-payments/code-server/pkg/solana" ) // @@ -109,8 +101,6 @@ func (p *service) worker(serviceCtx context.Context, state commitment.State, int // todo: needs to lock a distributed lock func (p *service) handle(ctx context.Context, record *commitment.Record) error { switch record.State { - case commitment.StateReadyToOpen: - return p.handleReadyToOpen(ctx, record) case commitment.StateOpen: return p.handleOpen(ctx, record) case commitment.StateClosed: @@ -120,36 +110,13 @@ func (p *service) handle(ctx context.Context, record *commitment.Record) error { } } -func (p *service) handleReadyToOpen(ctx context.Context, record *commitment.Record) error { - err := p.maybeMarkTreasuryAsRepaid(ctx, record) - if err != nil { - return nil - } - - shouldOpen, err := p.shouldOpenCommitmentVault(ctx, record) - if err != nil { - return err - } - - if !shouldOpen { - return p.maybeMarkCommitmentForGC(ctx, record) - } - - err = p.injectCommitmentVaultManagementFulfillments(ctx, record) - if err != nil { - return err - } - - return markCommitmentAsOpening(ctx, p.data, record.Intent, record.ActionId) -} - func (p *service) handleOpen(ctx context.Context, record *commitment.Record) error { err := p.maybeMarkTreasuryAsRepaid(ctx, record) if err != nil { - return nil + return err } - shouldClose, err := p.shouldCloseCommitmentVault(ctx, record) + shouldClose, err := p.shouldCloseCommitment(ctx, record) if err != nil { return err } @@ -164,56 +131,23 @@ func (p *service) handleOpen(ctx context.Context, record *commitment.Record) err func (p *service) handleClosed(ctx context.Context, record *commitment.Record) error { err := p.maybeMarkTreasuryAsRepaid(ctx, record) if err != nil { - return nil + return err } return p.maybeMarkCommitmentForGC(ctx, record) } -func (p *service) shouldOpenCommitmentVault(ctx context.Context, commitmentRecord *commitment.Record) (bool, error) { - privacyUpgradeDeadline, err := GetDeadlineToUpgradePrivacy(ctx, p.data, commitmentRecord) - if err != nil && err != ErrNoPrivacyUpgradeDeadline { - return false, err - } - - // The deadline to upgrade privacy has been reached. Open the commitment vault - // so we can unblock the scheduler from submitting the temporary private transfer. - if err != ErrNoPrivacyUpgradeDeadline && privacyUpgradeDeadline.Before(time.Now()) { - return true, nil - } - - // Otherwise, we must have at least one diverted repayment until we can open the - // account. This will unblock the scheduler from submitting diverted repayments - // to this commitment vault. - count, err := p.data.CountCommitmentRepaymentsDivertedToVault(ctx, commitmentRecord.Vault) - if err != nil { - return false, err +func (p *service) shouldCloseCommitment(ctx context.Context, commitmentRecord *commitment.Record) (bool, error) { + if commitmentRecord.State != commitment.StateOpen { + return false, nil } - return count > 0, nil -} - -func (p *service) shouldCloseCommitmentVault(ctx context.Context, commitmentRecord *commitment.Record) (bool, error) { // // Part 1: Ensure we either upgraded the temporary private transfer or confirmed it // - // The temporary private transfer could still be scheduled. - if commitmentRecord.RepaymentDivertedTo == nil { - fulfillmentRecords, err := p.data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, commitmentRecord.Intent, commitmentRecord.ActionId) - if err != nil { - return false, err - } - - if len(fulfillmentRecords) != 1 || *fulfillmentRecords[0].Destination != commitmentRecord.Vault { - return false, errors.New("fulfillment to check was not found") - } - - // The temporary private transfer isn't confirmed, so wait for an upgrade - // or the fulfillment to be played out on the blockchain before closing. - if fulfillmentRecords[0].State != fulfillment.StateConfirmed { - return false, nil - } + if commitmentRecord.RepaymentDivertedTo == nil && !commitmentRecord.TreasuryRepaid { + return false, nil } // @@ -234,7 +168,9 @@ func (p *service) shouldCloseCommitmentVault(ctx context.Context, commitmentReco } leafNode, err := merkleTree.GetLeafNode(ctx, commitmentAddressBytes) - if err != nil { + if err == merkletree.ErrLeafNotFound { + return false, nil + } else if err != nil { return false, err } @@ -259,59 +195,18 @@ func (p *service) shouldCloseCommitmentVault(ctx context.Context, commitmentReco // Part 3: Ensure all upgraded private transfers going to this commitment have been confirmed // - for _, scheduableState := range []fulfillment.State{ - fulfillment.StateUnknown, - fulfillment.StatePending, - } { - numPotentiallyInFlight, err := p.data.GetFulfillmentCountByTypeStateAndAddress( - ctx, - fulfillment.PermanentPrivacyTransferWithAuthority, - scheduableState, - commitmentRecord.Vault, - ) - if err != nil { - return false, err - } - - // If there are any diverted repayments that are, or potentially will be, - // scheduled, then leave the vault open. - if numPotentiallyInFlight > 0 { - return false, nil - } - } - - numConfirmed, err := p.data.GetFulfillmentCountByTypeStateAndAddress( - ctx, - fulfillment.PermanentPrivacyTransferWithAuthority, - fulfillment.StateConfirmed, - commitmentRecord.Vault, - ) - if err != nil { - return false, err - } - - numFailed, err := p.data.GetFulfillmentCountByTypeStateAndAddress( - ctx, - fulfillment.PermanentPrivacyTransferWithAuthority, - fulfillment.StateFailed, - commitmentRecord.Vault, - ) - if err != nil { - return false, err - } - - numDiverted, err := p.data.CountCommitmentRepaymentsDivertedToVault(ctx, commitmentRecord.Vault) + numPendingDivertedRepayments, err := p.data.CountPendingCommitmentRepaymentsDivertedToCommitment(ctx, commitmentRecord.Address) if err != nil { return false, err } - // Don't close if there are any failures. A human is needed. - if numFailed > 0 { + // All diverted repayments need to be confirmed before closing the commitment + if numPendingDivertedRepayments > 0 { return false, nil } - // All diverted repayments need to be confirmed before closing the vault - return numConfirmed >= numDiverted, nil + // todo: There isn't a way to close commitments yet in the VM + return false, nil } func (p *service) maybeMarkCommitmentForGC(ctx context.Context, commitmentRecord *commitment.Record) error { @@ -320,17 +215,9 @@ func (p *service) maybeMarkCommitmentForGC(ctx context.Context, commitmentRecord return nil } - // This commitment vault will never be opened, because the funds must have been - // diverted (or we'd be in the closed state) and a newer commitment vault will - // be used to divert new repayments. - if commitmentRecord.State == commitment.StateReadyToOpen { - return markCommitmentReadyForGC(ctx, p.data, commitmentRecord.Intent, commitmentRecord.ActionId) - } - - // This commitment vault will never be reopened. We only close the vault when - // all diverted repayments have been played out. If this commitment has also - // repaid the treasury, then it must have been through a temporary private transfer - // flow, or it was diverted itself to a different commitment. + // The commitment is closed because it has reached a terminal state and we + // are done with it. All temporary and permaenent cheques that are targets + // for this commitment should be cashed. Proceed to GC. if commitmentRecord.State == commitment.StateClosed { return markCommitmentReadyForGC(ctx, p.data, commitmentRecord.Intent, commitmentRecord.ActionId) } @@ -343,148 +230,20 @@ func (p *service) maybeMarkTreasuryAsRepaid(ctx context.Context, commitmentRecor return nil } - // If we haven't upgraded, then check the status of our commitment vault and whether - // it indicates the temporary private transfer was repaid. - if commitmentRecord.RepaymentDivertedTo == nil { - switch commitmentRecord.State { - case commitment.StateClosed, commitment.StateReadyToRemoveFromMerkleTree, commitment.StateRemovedFromMerkleTree: - default: - // The commitment isn't closed, so we can't say anything about repayment status. - return nil - } - - // The commitment is closed, so we know the treasury has been repaid via - // a temporary private transfer. We know we won't close a commitment until - // that happens. - return markTreasuryAsRepaid(ctx, p.data, commitmentRecord.Intent, commitmentRecord.ActionId) + fulfillmentTypeToCheck := fulfillment.TemporaryPrivacyTransferWithAuthority + if commitmentRecord.RepaymentDivertedTo != nil { + fulfillmentTypeToCheck = fulfillment.PermanentPrivacyTransferWithAuthority } - // Otherwise, check the status of the commitment we've upgraded to and whether - // the permanent private transfer was repaid. - divertedRecord, err := p.data.GetCommitmentByVault(ctx, *commitmentRecord.RepaymentDivertedTo) + fulfillmentRecords, err := p.data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillmentTypeToCheck, commitmentRecord.Intent, commitmentRecord.ActionId) if err != nil { return err } - switch divertedRecord.State { - case commitment.StateClosed, commitment.StateReadyToRemoveFromMerkleTree, commitment.StateRemovedFromMerkleTree: - // The commitment is closed, so we know the treasury has been repiad via a - // permantent priate transfer. We won't close a commitment until all repayments - // have been played out. - return markTreasuryAsRepaid(ctx, p.data, commitmentRecord.Intent, commitmentRecord.ActionId) - } - - return nil -} - -func (p *service) injectCommitmentVaultManagementFulfillments(ctx context.Context, commitmentRecord *commitment.Record) error { - // Idempotency check to ensure we don't double up on fulfillments - _, err := p.data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.InitializeCommitmentProof, commitmentRecord.Intent, commitmentRecord.ActionId) - if err == nil { + // The cheque hasn't been cashed, so we cannot mark the treasury as being repaid + if fulfillmentRecords[0].State != fulfillment.StateConfirmed { return nil - } else if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return err - } - - // Commitment vaults have no concept of blocks, intentionally so they're treated - // equally. This means we need to inject the fulfillments into the same intent - // where the commitment originated from, even though the private transfer repayment - // will likely get redirected to another commitment in a different intent. Because - // we said we'd treat them the same, it's best to think about how we think about - // repaying with temporary paying, and applying the same heuristic for permanent - // privacy. - intentRecord, err := p.data.GetIntent(ctx, commitmentRecord.Intent) - if err != nil { - return err - } - - txnAccounts, txnArgs, err := p.getCommitmentManagementTxnAccountsAndArgs(ctx, commitmentRecord) - if err != nil { - return err - } - - // Construct all fulfillment records - var fulfillmentsToSave []*fulfillment.Record - var noncesToReserve []*transaction.SelectedNonce - for i, txnToMake := range []struct { - fulfillmentType fulfillment.Type - ixns []solana.Instruction - }{ - {fulfillment.InitializeCommitmentProof, makeInitializeProofInstructions(txnAccounts, txnArgs)}, - {fulfillment.UploadCommitmentProof, makeUploadPartialProofInstructions(txnAccounts, txnArgs, 0, 20)}, - {fulfillment.UploadCommitmentProof, makeUploadPartialProofInstructions(txnAccounts, txnArgs, 21, 41)}, - {fulfillment.UploadCommitmentProof, makeUploadPartialProofInstructions(txnAccounts, txnArgs, 42, 62)}, // todo: Assumes merkle tree of depth 63 - {fulfillment.OpenCommitmentVault, append( - makeVerifyProofInstructions(txnAccounts, txnArgs), - makeOpenCommitmentVaultInstructions(txnAccounts, txnArgs)..., - )}, - {fulfillment.CloseCommitmentVault, makeCloseCommitmentVaultInstructions(txnAccounts, txnArgs)}, - } { - selectedNonce, err := transaction.SelectAvailableNonce(ctx, p.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) - if err != nil { - return err - } - defer func() { - selectedNonce.ReleaseIfNotReserved() - selectedNonce.Unlock() - }() - - txn, err := transaction.MakeNoncedTransaction(selectedNonce.Account, selectedNonce.Blockhash, txnToMake.ixns...) - if err != nil { - return err - } - txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) - - intentOrderingIndex := uint64(0) - fulfillmentOrderingIndex := uint32(i) - if txnToMake.fulfillmentType == fulfillment.CloseCommitmentVault { - intentOrderingIndex = uint64(math.MaxInt64) - fulfillmentOrderingIndex = uint32(0) - } - - fulfillmentRecord := &fulfillment.Record{ - Intent: intentRecord.IntentId, - IntentType: intentRecord.IntentType, - - ActionId: commitmentRecord.ActionId, - ActionType: action.PrivateTransfer, - - FulfillmentType: txnToMake.fulfillmentType, - Data: txn.Marshal(), - Signature: pointer.String(base58.Encode(txn.Signature())), - - Nonce: pointer.String(selectedNonce.Account.PublicKey().ToBase58()), - Blockhash: pointer.String(base58.Encode(selectedNonce.Blockhash[:])), - - Source: commitmentRecord.Vault, - - IntentOrderingIndex: intentOrderingIndex, - ActionOrderingIndex: 0, - FulfillmentOrderingIndex: fulfillmentOrderingIndex, - - DisableActiveScheduling: txnToMake.fulfillmentType != fulfillment.InitializeCommitmentProof, - - State: fulfillment.StateUnknown, - } - - fulfillmentsToSave = append(fulfillmentsToSave, fulfillmentRecord) - noncesToReserve = append(noncesToReserve, selectedNonce) } - // Creates all fulfillments and nonce reservations in a single DB transaction - return p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { - for i := 0; i < len(fulfillmentsToSave); i++ { - err = noncesToReserve[i].MarkReservedWithSignature(ctx, *fulfillmentsToSave[i].Signature) - if err != nil { - return err - } - } - - err = p.data.PutAllFulfillments(ctx, fulfillmentsToSave...) - if err != nil { - return err - } - - return nil - }) + return nil } diff --git a/pkg/code/async/commitment/worker_test.go b/pkg/code/async/commitment/worker_test.go index e9dcda5b..20caa17f 100644 --- a/pkg/code/async/commitment/worker_test.go +++ b/pkg/code/async/commitment/worker_test.go @@ -1,268 +1,3 @@ package async_commitment -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" -) - -func TestCommitmentWorker_StateReadyToOpen_RemainInState_PrivacyUpgraded(t *testing.T) { - env := setup(t) - env.generateAvailableNonces(t, 10) - - commitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateAddingLeaves(t, []*commitment.Record{commitmentRecord}) - - require.NoError(t, env.worker.handleReadyToOpen(env.ctx, commitmentRecord)) - env.assertCommitmentVaultManagementFulfillmentsNotInjected(t, commitmentRecord) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateReadyToOpen) - - // To trigger privacy deadline being met - env.simulateSourceAccountUnlocked(t, commitmentRecord) - - newerCommitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - require.NoError(t, env.data.SaveCommitment(env.ctx, newerCommitmentRecord)) - commitmentRecord.RepaymentDivertedTo = &newerCommitmentRecord.Vault - require.NoError(t, env.data.SaveCommitment(env.ctx, commitmentRecord)) - - require.NoError(t, env.worker.handleReadyToOpen(env.ctx, commitmentRecord)) - env.assertCommitmentVaultManagementFulfillmentsNotInjected(t, commitmentRecord) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateReadyToOpen) -} - -func TestCommitmentWorker_StateReadyToOpen_TransitionToStateOpening_TemporaryPrivacyDeadline(t *testing.T) { - env := setup(t) - env.generateAvailableNonces(t, 10) - - commitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateAddingLeaves(t, []*commitment.Record{commitmentRecord}) - - require.NoError(t, env.worker.handleReadyToOpen(env.ctx, commitmentRecord)) - env.assertCommitmentVaultManagementFulfillmentsNotInjected(t, commitmentRecord) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateReadyToOpen) - - env.simulateSourceAccountUnlocked(t, commitmentRecord) - - require.NoError(t, env.worker.handleReadyToOpen(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateOpening) - env.assertCommitmentVaultManagementFulfillmentsInjected(t, commitmentRecord) -} - -func TestCommitmentWorker_StateReadyToOpen_TransitionToStateOpening_TargetForPermanentPrivacyCheques(t *testing.T) { - env := setup(t) - env.generateAvailableNonces(t, 10) - - commitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateAddingLeaves(t, []*commitment.Record{commitmentRecord}) - - require.NoError(t, env.worker.handleReadyToOpen(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateReadyToOpen) - env.assertCommitmentVaultManagementFulfillmentsNotInjected(t, commitmentRecord) - - upgradedCommitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - upgradedCommitmentRecord.RepaymentDivertedTo = &commitmentRecord.Vault - require.NoError(t, env.data.SaveCommitment(env.ctx, upgradedCommitmentRecord)) - - require.NoError(t, env.worker.handleReadyToOpen(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateOpening) - env.assertCommitmentVaultManagementFulfillmentsInjected(t, commitmentRecord) -} - -func TestCommitmentWorker_StateReadyToOpen_TransitionToStateReadyToRemoveFromMerkleTree(t *testing.T) { - env := setup(t) - - commitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - - require.NoError(t, env.worker.handleClosed(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateReadyToOpen) - - commitmentRecord.TreasuryRepaid = true - require.NoError(t, env.data.SaveCommitment(env.ctx, commitmentRecord)) - - require.NoError(t, env.worker.handleClosed(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateReadyToRemoveFromMerkleTree) -} - -func TestCommitmentWorker_StateReadyToOpen_MarkTreasuryAsRepaid(t *testing.T) { - for _, state := range []commitment.State{ - commitment.StateReadyToOpen, - commitment.StateOpening, - commitment.StateOpen, - commitment.StateClosing, - commitment.StateClosed, - commitment.StateReadyToRemoveFromMerkleTree, - commitment.StateRemovedFromMerkleTree, - } { - env := setup(t) - - upgradedCommitment := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - divertedCommitment := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - - env.simulateCommitmentBeingUpgraded(t, upgradedCommitment, divertedCommitment) - - divertedCommitment.State = state - require.NoError(t, env.data.SaveCommitment(env.ctx, divertedCommitment)) - - require.NoError(t, env.worker.handleReadyToOpen(env.ctx, upgradedCommitment)) - env.assertCommitmentRepaymentStatus(t, upgradedCommitment.Address, state >= commitment.StateClosed) - env.assertCommitmentState(t, upgradedCommitment.Address, commitment.StateReadyToOpen) - } -} - -func TestCommitmentWorker_StateOpen_TransitionToStateClosing_TargetForPermanentPrivacyCheques(t *testing.T) { - for _, fulfillmentState := range []fulfillment.State{ - fulfillment.StateConfirmed, - fulfillment.StateFailed, - } { - env := setup(t) - env.generateAvailableNonces(t, 10) - - divertedCommitmentRecords := env.simulateCommitments(t, 10, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateAddingLeaves(t, divertedCommitmentRecords) - - commitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateOpen) - env.simulateAddingLeaves(t, []*commitment.Record{commitmentRecord}) - require.NoError(t, env.worker.injectCommitmentVaultManagementFulfillments(env.ctx, commitmentRecord)) - - futureCommitmentRecords := env.simulateCommitments(t, 10, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateAddingLeaves(t, futureCommitmentRecords) - - for _, divertedCommitmentRecord := range divertedCommitmentRecords { - env.simulateCommitmentBeingUpgraded(t, divertedCommitmentRecord, commitmentRecord) - } - env.simulateCommitmentBeingUpgraded(t, commitmentRecord, futureCommitmentRecords[0]) - - for _, divertedCommitmentRecord := range divertedCommitmentRecords { - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateOpen) - env.simulatePermanentPrivacyChequeCashed(t, divertedCommitmentRecord, fulfillmentState) - require.NoError(t, env.worker.handleOpen(env.ctx, commitmentRecord)) - } - - if fulfillmentState == fulfillment.StateConfirmed { - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateClosing) - } else { - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateOpen) - } - } -} - -func TestCommitmentWorker_StateOpen_TransitionToStateClosing_CashTemporaryPrivacyCheque(t *testing.T) { - for _, fulfillmentState := range []fulfillment.State{ - fulfillment.StateConfirmed, - fulfillment.StateFailed, - } { - env := setup(t) - env.generateAvailableNonces(t, 10) - - commitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateOpen) - env.simulateAddingLeaves(t, []*commitment.Record{commitmentRecord}) - require.NoError(t, env.worker.injectCommitmentVaultManagementFulfillments(env.ctx, commitmentRecord)) - - futureCommitmentRecords := env.simulateCommitments(t, 10, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateAddingLeaves(t, futureCommitmentRecords) - - require.NoError(t, env.worker.handleOpen(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateOpen) - - env.simulateTemporaryPrivacyChequeCashed(t, commitmentRecord, fulfillmentState) - - require.NoError(t, env.worker.handleOpen(env.ctx, commitmentRecord)) - if fulfillmentState == fulfillment.StateConfirmed { - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateClosing) - } else { - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateOpen) - } - } -} - -func TestCommitmentWorker_StateOpen_TransitionToStateClosing_PrivacyUpgradeCandidateTimeout(t *testing.T) { - env := setup(t) - env.generateAvailableNonces(t, 10) - - commitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateOpen) - env.simulateAddingLeaves(t, []*commitment.Record{commitmentRecord}) - require.NoError(t, env.worker.injectCommitmentVaultManagementFulfillments(env.ctx, commitmentRecord)) - env.simulateTemporaryPrivacyChequeCashed(t, commitmentRecord, fulfillment.StateConfirmed) - - otherCommitmentRecords := env.simulateCommitments(t, 10, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateAddingLeaves(t, otherCommitmentRecords) - - privacyUpgradeCandidateSelectionTimeout = 100 * time.Millisecond - require.NoError(t, env.worker.handleOpen(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateOpen) - - time.Sleep(2 * privacyUpgradeCandidateSelectionTimeout) - require.NoError(t, env.worker.handleOpen(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateClosing) -} - -func TestCommitmentWorker_StateOpen_MarkTreasuryAsRepaid(t *testing.T) { - for _, state := range []commitment.State{ - commitment.StateReadyToOpen, - commitment.StateOpening, - commitment.StateOpen, - commitment.StateClosing, - commitment.StateClosed, - commitment.StateReadyToRemoveFromMerkleTree, - commitment.StateRemovedFromMerkleTree, - } { - env := setup(t) - - upgradedCommitment := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateOpen) - divertedCommitment := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - - env.simulateCommitmentBeingUpgraded(t, upgradedCommitment, divertedCommitment) - - divertedCommitment.State = state - require.NoError(t, env.data.SaveCommitment(env.ctx, divertedCommitment)) - - require.NoError(t, env.worker.handleReadyToOpen(env.ctx, upgradedCommitment)) - env.assertCommitmentRepaymentStatus(t, upgradedCommitment.Address, state >= commitment.StateClosed) - env.assertCommitmentState(t, upgradedCommitment.Address, commitment.StateOpen) - } -} - -func TestCommitmentWorker_StateClosed_TransitionToStateReadyToRemoveFromMerkleTree(t *testing.T) { - env := setup(t) - - commitmentRecord := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateClosed) - - require.NoError(t, env.worker.handleClosed(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateClosed) - - commitmentRecord.TreasuryRepaid = true - require.NoError(t, env.data.SaveCommitment(env.ctx, commitmentRecord)) - - require.NoError(t, env.worker.handleClosed(env.ctx, commitmentRecord)) - env.assertCommitmentState(t, commitmentRecord.Address, commitment.StateReadyToRemoveFromMerkleTree) -} - -func TestCommitmentWorker_StateClosed_MarkTreasuryAsRepaid(t *testing.T) { - for _, state := range []commitment.State{ - commitment.StateReadyToOpen, - commitment.StateOpening, - commitment.StateOpen, - commitment.StateClosing, - commitment.StateClosed, - commitment.StateReadyToRemoveFromMerkleTree, - commitment.StateRemovedFromMerkleTree, - } { - env := setup(t) - - upgradedCommitment := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateClosed) - divertedCommitment := env.simulateCommitment(t, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - - env.simulateCommitmentBeingUpgraded(t, upgradedCommitment, divertedCommitment) - - divertedCommitment.State = state - require.NoError(t, env.data.SaveCommitment(env.ctx, divertedCommitment)) - - require.NoError(t, env.worker.handleClosed(env.ctx, upgradedCommitment)) - env.assertCommitmentRepaymentStatus(t, upgradedCommitment.Address, state >= commitment.StateClosed) - env.assertCommitmentState(t, upgradedCommitment.Address, commitment.StateClosed) - } -} +// todo: add tests once commitment flows are finalized diff --git a/pkg/code/async/treasury/testutil.go b/pkg/code/async/treasury/testutil.go index 4fb46b59..6cb6beff 100644 --- a/pkg/code/async/treasury/testutil.go +++ b/pkg/code/async/treasury/testutil.go @@ -177,15 +177,12 @@ func (e *testEnv) simulateCommitments(t *testing.T, count int, recentRoot string var commitmentRecords []*commitment.Record for i := 0; i < count; i++ { commitmentRecord := &commitment.Record{ - DataVersion: splitter_token.DataVersion1, - - Pool: e.treasuryPool.Address, Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Vault: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + Pool: e.treasuryPool.Address, RecentRoot: recentRoot, - Transcript: "transcript", + Transcript: "transcript", Destination: testutil.NewRandomAccount(t).PublicKey().ToBase58(), Amount: kin.ToQuarks(1), diff --git a/pkg/code/data/commitment/commitment.go b/pkg/code/data/commitment/commitment.go index 0cd5a155..0d1fe459 100644 --- a/pkg/code/data/commitment/commitment.go +++ b/pkg/code/data/commitment/commitment.go @@ -4,7 +4,7 @@ import ( "errors" "time" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" + "github.com/code-payments/code-server/pkg/pointer" ) type State uint8 @@ -12,8 +12,8 @@ type State uint8 const ( StateUnknown State = iota StatePayingDestination - StateReadyToOpen - StateOpening + StateReadyToOpen // No longer valid in the CVM + StateOpening // No longer valid in the CVM StateOpen StateClosing StateClosed @@ -27,22 +27,15 @@ const ( type Record struct { Id uint64 - DataVersion splitter_token.DataVersion - Address string - Bump uint8 Pool string - PoolBump uint8 RecentRoot string - Transcript string + Transcript string Destination string Amount uint64 - Vault string - VaultBump uint8 - Intent string ActionId uint32 @@ -52,7 +45,7 @@ type Record struct { // Not to be confused with payments being diverted to this commitment and then // being closed. TreasuryRepaid bool - // The commitment vault where repayment for Record.Amount will be diverted to. + // The commitment where repayment for Record.Amount will be diverted to. RepaymentDivertedTo *string State State @@ -61,10 +54,6 @@ type Record struct { } func (r *Record) Validate() error { - if r.DataVersion != splitter_token.DataVersion1 { - return errors.New("commitment data version must be 1") - } - if len(r.Address) == 0 { return errors.New("address is required") } @@ -89,10 +78,6 @@ func (r *Record) Validate() error { return errors.New("settlement amount must be positive") } - if len(r.Vault) == 0 { - return errors.New("vault is required") - } - if len(r.Intent) == 0 { return errors.New("intent is required") } @@ -105,38 +90,25 @@ func (r *Record) Validate() error { } func (r *Record) Clone() *Record { - var repaymentDivertedTo *string - if r.RepaymentDivertedTo != nil { - value := *r.RepaymentDivertedTo - repaymentDivertedTo = &value - } - return &Record{ Id: r.Id, - DataVersion: r.DataVersion, - Address: r.Address, - Bump: r.Bump, Pool: r.Pool, - PoolBump: r.PoolBump, RecentRoot: r.RecentRoot, - Transcript: r.Transcript, + Transcript: r.Transcript, Destination: r.Destination, Amount: r.Amount, - Vault: r.Vault, - VaultBump: r.VaultBump, - Intent: r.Intent, ActionId: r.ActionId, Owner: r.Owner, TreasuryRepaid: r.TreasuryRepaid, - RepaymentDivertedTo: repaymentDivertedTo, + RepaymentDivertedTo: pointer.StringCopy(r.RepaymentDivertedTo), State: r.State, @@ -147,29 +119,22 @@ func (r *Record) Clone() *Record { func (r *Record) CopyTo(dst *Record) { dst.Id = r.Id - dst.DataVersion = r.DataVersion - dst.Address = r.Address - dst.Bump = r.Bump dst.Pool = r.Pool - dst.PoolBump = r.PoolBump dst.RecentRoot = r.RecentRoot - dst.Transcript = r.Transcript + dst.Transcript = r.Transcript dst.Destination = r.Destination dst.Amount = r.Amount - dst.Vault = r.Vault - dst.VaultBump = r.VaultBump - dst.Intent = r.Intent dst.ActionId = r.ActionId dst.Owner = r.Owner dst.TreasuryRepaid = r.TreasuryRepaid - dst.RepaymentDivertedTo = r.RepaymentDivertedTo + dst.RepaymentDivertedTo = pointer.StringCopy(r.RepaymentDivertedTo) dst.State = r.State diff --git a/pkg/code/data/commitment/memory/store.go b/pkg/code/data/commitment/memory/store.go index d8a8ac1f..26bb6f7a 100644 --- a/pkg/code/data/commitment/memory/store.go +++ b/pkg/code/data/commitment/memory/store.go @@ -6,8 +6,8 @@ import ( "sync" "time" - "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/code/data/commitment" + "github.com/code-payments/code-server/pkg/database/query" ) type ById []*commitment.Record @@ -84,18 +84,6 @@ func (s *store) GetByAddress(_ context.Context, address string) (*commitment.Rec return nil, commitment.ErrCommitmentNotFound } -// GetByVault implements commitment.Store.GetByVault -func (s *store) GetByVault(_ context.Context, vault string) (*commitment.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if item := s.findByVault(vault); item != nil { - return item.Clone(), nil - } - - return nil, commitment.ErrCommitmentNotFound -} - // GetByAction implements commitment.Store.GetByAction func (s *store) GetByAction(_ context.Context, intentId string, actionId uint32) (*commitment.Record, error) { s.mu.Lock() @@ -182,12 +170,13 @@ func (s *store) CountByState(_ context.Context, state commitment.State) (uint64, return uint64(len(items)), nil } -// CountRepaymentsDivertedToVault implements commitment.Store.CountRepaymentsDivertedToVault -func (s *store) CountRepaymentsDivertedToVault(_ context.Context, vault string) (uint64, error) { +// CountPendingRepaymentsDivertedToCommitment implements commitment.Store.CountPendingRepaymentsDivertedToCommitment +func (s *store) CountPendingRepaymentsDivertedToCommitment(_ context.Context, address string) (uint64, error) { s.mu.Lock() defer s.mu.Unlock() - items := s.findByRepaymentsDivertedToVault(vault) + items := s.findByRepaymentsDivertedToCommitment(address) + items = s.filterByRepaymentStatus(items, false) return uint64(len(items)), nil } @@ -212,15 +201,6 @@ func (s *store) findByAddress(address string) *commitment.Record { return nil } -func (s *store) findByVault(vault string) *commitment.Record { - for _, item := range s.records { - if item.Vault == vault { - return item - } - } - return nil -} - func (s *store) findByAction(intentId string, actionId uint32) *commitment.Record { for _, item := range s.records { if item.Intent == intentId && item.ActionId == actionId { @@ -240,10 +220,10 @@ func (s *store) findByPool(pool string) []*commitment.Record { return res } -func (s *store) findByRepaymentsDivertedToVault(vault string) []*commitment.Record { +func (s *store) findByRepaymentsDivertedToCommitment(address string) []*commitment.Record { var res []*commitment.Record for _, item := range s.records { - if item.RepaymentDivertedTo != nil && *item.RepaymentDivertedTo == vault { + if item.RepaymentDivertedTo != nil && *item.RepaymentDivertedTo == address { res = append(res, item) } } diff --git a/pkg/code/data/commitment/postgres/model.go b/pkg/code/data/commitment/postgres/model.go index bfacd102..b188fd43 100644 --- a/pkg/code/data/commitment/postgres/model.go +++ b/pkg/code/data/commitment/postgres/model.go @@ -7,10 +7,10 @@ import ( "github.com/jmoiron/sqlx" + "github.com/code-payments/code-server/pkg/code/data/commitment" pgutil "github.com/code-payments/code-server/pkg/database/postgres" q "github.com/code-payments/code-server/pkg/database/query" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/code/data/commitment" + "github.com/code-payments/code-server/pkg/pointer" ) const ( @@ -20,22 +20,15 @@ const ( type model struct { Id sql.NullInt64 `db:"id"` - DataVersion uint `db:"data_version"` - Address string `db:"address"` - Bump uint `db:"bump"` Pool string `db:"pool"` - PoolBump uint `db:"pool_bump"` RecentRoot string `db:"recent_root"` - Transcript string `db:"transcript"` + Transcript string `db:"transcript"` Destination string `db:"destination"` Amount uint64 `db:"amount"` - Vault string `db:"vault"` - VaultBump uint `db:"vault_bump"` - Intent string `db:"intent"` ActionId uint `db:"action_id"` @@ -54,36 +47,26 @@ func toModel(obj *commitment.Record) (*model, error) { return nil, err } - var repaymentDivertedTo sql.NullString - if obj.RepaymentDivertedTo != nil { - repaymentDivertedTo.Valid = true - repaymentDivertedTo.String = *obj.RepaymentDivertedTo - } - return &model{ - DataVersion: uint(obj.DataVersion), - Address: obj.Address, - Bump: uint(obj.Bump), Pool: obj.Pool, - PoolBump: uint(obj.PoolBump), RecentRoot: obj.RecentRoot, - Transcript: obj.Transcript, + Transcript: obj.Transcript, Destination: obj.Destination, Amount: obj.Amount, - Vault: obj.Vault, - VaultBump: uint(obj.VaultBump), - Intent: obj.Intent, ActionId: uint(obj.ActionId), Owner: obj.Owner, - TreasuryRepaid: obj.TreasuryRepaid, - RepaymentDivertedTo: repaymentDivertedTo, + TreasuryRepaid: obj.TreasuryRepaid, + RepaymentDivertedTo: sql.NullString{ + Valid: obj.RepaymentDivertedTo != nil, + String: *pointer.StringOrDefault(obj.RepaymentDivertedTo, ""), + }, State: uint(obj.State), @@ -92,49 +75,37 @@ func toModel(obj *commitment.Record) (*model, error) { } func fromModel(obj *model) *commitment.Record { - record := &commitment.Record{ + return &commitment.Record{ Id: uint64(obj.Id.Int64), - DataVersion: splitter_token.DataVersion(obj.DataVersion), - Address: obj.Address, - Bump: uint8(obj.Bump), Pool: obj.Pool, - PoolBump: uint8(obj.PoolBump), RecentRoot: obj.RecentRoot, - Transcript: obj.Transcript, + Transcript: obj.Transcript, Destination: obj.Destination, Amount: obj.Amount, - Vault: obj.Vault, - VaultBump: uint8(obj.VaultBump), - Intent: obj.Intent, ActionId: uint32(obj.ActionId), Owner: obj.Owner, - TreasuryRepaid: obj.TreasuryRepaid, + TreasuryRepaid: obj.TreasuryRepaid, + RepaymentDivertedTo: pointer.StringIfValid(obj.RepaymentDivertedTo.Valid, obj.RepaymentDivertedTo.String), State: commitment.State(obj.State), CreatedAt: obj.CreatedAt, } - - if obj.RepaymentDivertedTo.Valid { - record.RepaymentDivertedTo = &obj.RepaymentDivertedTo.String - } - - return record } func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { divertedToCondition := tableName + ".repayment_diverted_to IS NULL" if m.RepaymentDivertedTo.Valid { - divertedToCondition = "(" + tableName + ".repayment_diverted_to IS NULL OR " + tableName + ".repayment_diverted_to = $16)" + divertedToCondition = "(" + tableName + ".repayment_diverted_to IS NULL OR " + tableName + ".repayment_diverted_to = $11)" } treasuryRepaidCondition := tableName + ".treasury_repaid IS FALSE" @@ -147,16 +118,16 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { // Luckily, all updateable state-like fields should progress forward in a // predictable manner, making conditions easy to reason about. query := `INSERT INTO ` + tableName + ` - (data_version, address, bump, pool, pool_bump, recent_root, transcript, destination, amount, vault, vault_bump, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + (address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ON CONFLICT (address) DO UPDATE - SET treasury_repaid = $15 OR ` + tableName + `.treasury_repaid , repayment_diverted_to = COALESCE($16, ` + tableName + `.repayment_diverted_to), state = GREATEST($17, ` + tableName + `.state) - WHERE ` + tableName + `.address = $2 AND ` + treasuryRepaidCondition + ` AND ` + divertedToCondition + ` AND ` + tableName + `.state <= $17 + SET treasury_repaid = $10 OR ` + tableName + `.treasury_repaid , repayment_diverted_to = COALESCE($11, ` + tableName + `.repayment_diverted_to), state = GREATEST($12, ` + tableName + `.state) + WHERE ` + tableName + `.address = $1 AND ` + treasuryRepaidCondition + ` AND ` + divertedToCondition + ` AND ` + tableName + `.state <= $12 RETURNING - id, data_version, address, bump, pool, pool_bump, recent_root, transcript, destination, amount, vault, vault_bump, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at` + id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at` if m.CreatedAt.IsZero() { m.CreatedAt = time.Now() @@ -165,17 +136,12 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { err := tx.QueryRowxContext( ctx, query, - m.DataVersion, m.Address, - m.Bump, m.Pool, - m.PoolBump, m.RecentRoot, m.Transcript, m.Destination, m.Amount, - m.Vault, - m.VaultBump, m.Intent, m.ActionId, m.Owner, @@ -192,7 +158,7 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*model, error) { res := &model{} - query := `SELECT id, data_version, address, bump, pool, pool_bump, recent_root, transcript, destination, amount, vault, vault_bump, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + query := `SELECT id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at FROM ` + tableName + ` WHERE address = $1 LIMIT 1` @@ -204,25 +170,10 @@ func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*model, e return res, nil } -func dbGetByVault(ctx context.Context, db *sqlx.DB, vault string) (*model, error) { - res := &model{} - - query := `SELECT id, data_version, address, bump, pool, pool_bump, recent_root, transcript, destination, amount, vault, vault_bump, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at - FROM ` + tableName + ` - WHERE vault = $1 - LIMIT 1` - - err := db.GetContext(ctx, res, query, vault) - if err != nil { - return nil, pgutil.CheckNoRows(err, commitment.ErrCommitmentNotFound) - } - return res, nil -} - func dbGetByAction(ctx context.Context, db *sqlx.DB, intentId string, actionId uint32) (*model, error) { res := &model{} - query := `SELECT id, data_version, address, bump, pool, pool_bump, recent_root, transcript, destination, amount, vault, vault_bump, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + query := `SELECT id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at FROM ` + tableName + ` WHERE intent = $1 AND action_id = $2 LIMIT 1` @@ -237,7 +188,7 @@ func dbGetByAction(ctx context.Context, db *sqlx.DB, intentId string, actionId u func dbGetAllByState(ctx context.Context, db *sqlx.DB, state commitment.State, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*model, error) { res := []*model{} - query := `SELECT id, data_version, address, bump, pool, pool_bump, recent_root, transcript, destination, amount, vault, vault_bump, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + query := `SELECT id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at FROM ` + tableName + ` WHERE (state = $1) ` @@ -259,7 +210,7 @@ func dbGetAllByState(ctx context.Context, db *sqlx.DB, state commitment.State, c func dbGetUpgradeableByOwner(ctx context.Context, db *sqlx.DB, owner string, limit uint64) ([]*model, error) { res := []*model{} - query := `SELECT id, data_version, address, bump, pool, pool_bump, recent_root, transcript, destination, amount, vault, vault_bump, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + query := `SELECT id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at FROM ` + tableName + ` WHERE owner = $1 AND state > $3 AND state < $4 AND repayment_diverted_to IS NULL LIMIT $2 @@ -337,18 +288,18 @@ func dbCountByState(ctx context.Context, db *sqlx.DB, state commitment.State) (u return res, nil } -func dbCountRepaymentsDivertedToVault(ctx context.Context, db *sqlx.DB, vault string) (uint64, error) { +func dbCountPendingRepaymentsDivertedToCommitment(ctx context.Context, db *sqlx.DB, commitment string) (uint64, error) { var res uint64 query := `SELECT COUNT(*) FROM ` + tableName + ` - WHERE repayment_diverted_to = $1 + WHERE repayment_diverted_to = $1 AND NOT treasury_repaid ` err := db.GetContext( ctx, &res, query, - vault, + commitment, ) if err != nil { return 0, err diff --git a/pkg/code/data/commitment/postgres/store.go b/pkg/code/data/commitment/postgres/store.go index 17254119..eabff4bc 100644 --- a/pkg/code/data/commitment/postgres/store.go +++ b/pkg/code/data/commitment/postgres/store.go @@ -6,8 +6,8 @@ import ( "github.com/jmoiron/sqlx" - "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/code/data/commitment" + "github.com/code-payments/code-server/pkg/database/query" ) type store struct { @@ -48,16 +48,6 @@ func (s *store) GetByAddress(ctx context.Context, address string) (*commitment.R return fromModel(model), nil } -// GetByVault implements commitment.Store.GetByVault -func (s *store) GetByVault(ctx context.Context, vault string) (*commitment.Record, error) { - model, err := dbGetByVault(ctx, s.db, vault) - if err != nil { - return nil, err - } - - return fromModel(model), nil -} - // GetByAction implements commitment.Store.GetByAction func (s *store) GetByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) { model, err := dbGetByAction(ctx, s.db, intentId, actionId) @@ -111,7 +101,7 @@ func (s *store) CountByState(ctx context.Context, state commitment.State) (uint6 return dbCountByState(ctx, s.db, state) } -// CountRepaymentsDivertedToVault implements commitment.Store.CountRepaymentsDivertedToVault -func (s *store) CountRepaymentsDivertedToVault(ctx context.Context, vault string) (uint64, error) { - return dbCountRepaymentsDivertedToVault(ctx, s.db, vault) +// CountPendingRepaymentsDivertedToCommitment implements commitment.Store.CountPendingRepaymentsDivertedToCommitment +func (s *store) CountPendingRepaymentsDivertedToCommitment(ctx context.Context, address string) (uint64, error) { + return dbCountPendingRepaymentsDivertedToCommitment(ctx, s.db, address) } diff --git a/pkg/code/data/commitment/postgres/store_test.go b/pkg/code/data/commitment/postgres/store_test.go index ab1dc518..cc3c8598 100644 --- a/pkg/code/data/commitment/postgres/store_test.go +++ b/pkg/code/data/commitment/postgres/store_test.go @@ -22,22 +22,15 @@ const ( CREATE TABLE codewallet__core_commitment( id SERIAL NOT NULL PRIMARY KEY, - data_version INTEGER NOT NULL, - address TEXT NOT NULL, - bump INTEGER NOT NULL, pool TEXT NOT NULL, - pool_bump INTEGER NOT NULL, recent_root TEXT NOT NULL, - transcript TEXT NOT NULL, + transcript TEXT NOT NULL, destination TEXT NOT NULL, amount BIGINT NOT NULL CHECK (amount >= 0), - vault TEXT NOT NULL, - vault_bump INTEGER NOT NULL, - intent TEXT NOT NULL, action_id INTEGER NOT NULL, @@ -52,7 +45,6 @@ const ( CONSTRAINT codewallet__core_commitment__uniq__address UNIQUE (address), CONSTRAINT codewallet__core_commitment__uniq__transcript UNIQUE (transcript), - CONSTRAINT codewallet__core_commitment__uniq__vault UNIQUE (vault), CONSTRAINT codewallet__core_commitment__uniq__intent__and__action_id UNIQUE (intent, action_id) ); ` diff --git a/pkg/code/data/commitment/store.go b/pkg/code/data/commitment/store.go index eecdb56d..647080d1 100644 --- a/pkg/code/data/commitment/store.go +++ b/pkg/code/data/commitment/store.go @@ -19,9 +19,6 @@ type Store interface { // GetByAddress gets a commitment account's state by its address GetByAddress(ctx context.Context, address string) (*Record, error) - // GetByVault gets a commitment account's state by the vault address - GetByVault(ctx context.Context, vault string) (*Record, error) - // GetByAction gets a commitment account's state by the action it's involved in GetByAction(ctx context.Context, intentId string, actionId uint32) (*Record, error) @@ -43,7 +40,7 @@ type Store interface { // CountByState counts the number of commitment records in a given state CountByState(ctx context.Context, state State) (uint64, error) - // CountRepaymentsDivertedToVault counts the number of commitments whose repayments - // are diverted to the provided vault. - CountRepaymentsDivertedToVault(ctx context.Context, vault string) (uint64, error) + // CountPendingRepaymentsDivertedToCommitment counts the number of commitments whose + // pending repayments are diverted to the provided one. + CountPendingRepaymentsDivertedToCommitment(ctx context.Context, address string) (uint64, error) } diff --git a/pkg/code/data/commitment/tests/tests.go b/pkg/code/data/commitment/tests/tests.go index 6bf4bf11..6d2d2e24 100644 --- a/pkg/code/data/commitment/tests/tests.go +++ b/pkg/code/data/commitment/tests/tests.go @@ -10,9 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/database/query" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" "github.com/code-payments/code-server/pkg/code/data/commitment" + "github.com/code-payments/code-server/pkg/database/query" ) func RunTests(t *testing.T, s commitment.Store, teardown func()) { @@ -34,22 +33,15 @@ func testRoundTrip(t *testing.T, s commitment.Store) { ctx := context.Background() expected := &commitment.Record{ - DataVersion: splitter_token.DataVersion1, - Address: "address", - Bump: 255, Pool: "pool", - PoolBump: 254, RecentRoot: "root", - Transcript: "transcript", + Transcript: "transcript", Destination: "destination", Amount: 12345, - Vault: "vault", - VaultBump: 253, - Intent: "intent", ActionId: 1, @@ -63,9 +55,6 @@ func testRoundTrip(t *testing.T, s commitment.Store) { _, err := s.GetByAddress(ctx, expected.Address) assert.Equal(t, commitment.ErrCommitmentNotFound, err) - _, err = s.GetByVault(ctx, expected.Vault) - assert.Equal(t, commitment.ErrCommitmentNotFound, err) - _, err = s.GetByAction(ctx, expected.Intent, expected.ActionId) assert.Equal(t, commitment.ErrCommitmentNotFound, err) @@ -77,17 +66,13 @@ func testRoundTrip(t *testing.T, s commitment.Store) { require.NoError(t, err) assertEquivalentRecords(t, expected, actual) - actual, err = s.GetByVault(ctx, expected.Vault) - require.NoError(t, err) - assertEquivalentRecords(t, expected, actual) - actual, err = s.GetByAction(ctx, expected.Intent, expected.ActionId) require.NoError(t, err) assertEquivalentRecords(t, expected, actual) - otherCommitmentVault := "other-vault" + otherCommitment := "other-commitment" expected.TreasuryRepaid = true - expected.RepaymentDivertedTo = &otherCommitmentVault + expected.RepaymentDivertedTo = &otherCommitment expected.State = commitment.StateClosed cloned = expected.Clone() require.NoError(t, s.Save(ctx, cloned)) @@ -96,10 +81,6 @@ func testRoundTrip(t *testing.T, s commitment.Store) { require.NoError(t, err) assertEquivalentRecords(t, expected, actual) - actual, err = s.GetByVault(ctx, expected.Vault) - require.NoError(t, err) - assertEquivalentRecords(t, expected, actual) - actual, err = s.GetByAction(ctx, expected.Intent, expected.ActionId) require.NoError(t, err) assertEquivalentRecords(t, expected, actual) @@ -110,32 +91,25 @@ func testUpdateConstraints(t *testing.T, s commitment.Store) { t.Run("testUpdateConstraints", func(t *testing.T) { ctx := context.Background() - otherCommitmentVault1 := "other-vault-1" - otherCommitmentVault2 := "other-vault-2" + otherCommitmentCommitment1 := "other-commitment-1" + otherCommitmentCommitment2 := "other-commitment-2" expected := &commitment.Record{ - DataVersion: splitter_token.DataVersion1, - Address: "address", - Bump: 255, Pool: "pool", - PoolBump: 254, RecentRoot: "root", Transcript: "transcript", Destination: "destination", Amount: 12345, - Vault: "vault", - VaultBump: 253, - Intent: "intent", ActionId: 1, Owner: "owner", TreasuryRepaid: true, - RepaymentDivertedTo: &otherCommitmentVault1, + RepaymentDivertedTo: &otherCommitmentCommitment1, State: commitment.StateClosed, @@ -156,7 +130,7 @@ func testUpdateConstraints(t *testing.T, s commitment.Store) { assert.Equal(t, commitment.ErrInvalidCommitment, s.Save(ctx, cloned)) cloned = expected.Clone() - cloned.RepaymentDivertedTo = &otherCommitmentVault2 + cloned.RepaymentDivertedTo = &otherCommitmentCommitment2 assert.Equal(t, commitment.ErrInvalidCommitment, s.Save(ctx, cloned)) cloned = expected.Clone() @@ -176,11 +150,11 @@ func testGetAllByState(t *testing.T, s commitment.Store) { assert.Equal(t, commitment.ErrCommitmentNotFound, err) expected := []*commitment.Record{ - {DataVersion: splitter_token.DataVersion1, Address: "commitment1", Vault: "vault1", Pool: "pool", RecentRoot: "root", Transcript: "transcript1", Intent: "intent", ActionId: 0, Owner: "owner1", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {DataVersion: splitter_token.DataVersion1, Address: "commitment2", Vault: "vault2", Pool: "pool", RecentRoot: "root", Transcript: "transcript2", Intent: "intent", ActionId: 1, Owner: "owner2", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {DataVersion: splitter_token.DataVersion1, Address: "commitment3", Vault: "vault3", Pool: "pool", RecentRoot: "root", Transcript: "transcript3", Intent: "intent", ActionId: 2, Owner: "owner3", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {DataVersion: splitter_token.DataVersion1, Address: "commitment4", Vault: "vault4", Pool: "pool", RecentRoot: "root", Transcript: "transcript4", Intent: "intent", ActionId: 3, Owner: "owner4", Destination: "destination", Amount: 123, State: commitment.StateClosed}, - {DataVersion: splitter_token.DataVersion1, Address: "commitment5", Vault: "vault5", Pool: "pool", RecentRoot: "root", Transcript: "transcript5", Intent: "intent", ActionId: 4, Owner: "owner5", Destination: "destination", Amount: 123, State: commitment.StateClosed}, + {Address: "commitment1", Pool: "pool", RecentRoot: "root", Transcript: "transcript1", Intent: "intent", ActionId: 0, Owner: "owner1", Destination: "destination", Amount: 123, State: commitment.StateOpen}, + {Address: "commitment2", Pool: "pool", RecentRoot: "root", Transcript: "transcript2", Intent: "intent", ActionId: 1, Owner: "owner2", Destination: "destination", Amount: 123, State: commitment.StateOpen}, + {Address: "commitment3", Pool: "pool", RecentRoot: "root", Transcript: "transcript3", Intent: "intent", ActionId: 2, Owner: "owner3", Destination: "destination", Amount: 123, State: commitment.StateOpen}, + {Address: "commitment4", Pool: "pool", RecentRoot: "root", Transcript: "transcript4", Intent: "intent", ActionId: 3, Owner: "owner4", Destination: "destination", Amount: 123, State: commitment.StateClosed}, + {Address: "commitment5", Pool: "pool", RecentRoot: "root", Transcript: "transcript5", Intent: "intent", ActionId: 4, Owner: "owner5", Destination: "destination", Amount: 123, State: commitment.StateClosed}, } for _, record := range expected { require.NoError(t, s.Save(ctx, record)) @@ -250,33 +224,31 @@ func testGetUpgradeableByOwner(t *testing.T, s commitment.Store) { _, err := s.GetUpgradeableByOwner(ctx, "owner", 10) assert.Equal(t, commitment.ErrCommitmentNotFound, err) - futureVault := "future-vault" + futureCommitment := "future-commitment" records := []*commitment.Record{ {State: commitment.StateUnknown, Owner: "owner1"}, {State: commitment.StatePayingDestination, Owner: "owner1"}, {State: commitment.StateReadyToOpen, Owner: "owner1"}, - {State: commitment.StateReadyToOpen, Owner: "owner1", RepaymentDivertedTo: &futureVault}, + {State: commitment.StateReadyToOpen, Owner: "owner1", RepaymentDivertedTo: &futureCommitment}, {State: commitment.StateOpening, Owner: "owner2"}, - {State: commitment.StateOpening, Owner: "owner2", RepaymentDivertedTo: &futureVault}, + {State: commitment.StateOpening, Owner: "owner2", RepaymentDivertedTo: &futureCommitment}, {State: commitment.StateOpen, Owner: "owner2"}, - {State: commitment.StateOpen, Owner: "owner2", RepaymentDivertedTo: &futureVault}, + {State: commitment.StateOpen, Owner: "owner2", RepaymentDivertedTo: &futureCommitment}, {State: commitment.StateClosing, Owner: "owner2"}, - {State: commitment.StateClosing, Owner: "owner2", RepaymentDivertedTo: &futureVault}, + {State: commitment.StateClosing, Owner: "owner2", RepaymentDivertedTo: &futureCommitment}, {State: commitment.StateClosed, Owner: "owner2"}, - {State: commitment.StateClosed, Owner: "owner2", RepaymentDivertedTo: &futureVault}, + {State: commitment.StateClosed, Owner: "owner2", RepaymentDivertedTo: &futureCommitment}, {State: commitment.StateReadyToRemoveFromMerkleTree, Owner: "owner1"}, - {State: commitment.StateReadyToRemoveFromMerkleTree, Owner: "owner1", RepaymentDivertedTo: &futureVault}, + {State: commitment.StateReadyToRemoveFromMerkleTree, Owner: "owner1", RepaymentDivertedTo: &futureCommitment}, {State: commitment.StateRemovedFromMerkleTree, Owner: "owner1"}, - {State: commitment.StateRemovedFromMerkleTree, Owner: "owner1", RepaymentDivertedTo: &futureVault}, + {State: commitment.StateRemovedFromMerkleTree, Owner: "owner1", RepaymentDivertedTo: &futureCommitment}, } for i, record := range records { // Populate data irrelevant to test - record.DataVersion = splitter_token.DataVersion1 record.Pool = "pool" record.RecentRoot = "root" record.Address = fmt.Sprintf("address%d", i) - record.Vault = fmt.Sprintf("vault%d", i) record.RecentRoot = fmt.Sprintf("root%d", i) record.Transcript = fmt.Sprintf("transcript%d", i) record.Destination = fmt.Sprintf("destination%d", i) @@ -333,9 +305,7 @@ func testGetTreasuryPoolDeficit(t *testing.T, s commitment.Store) { record.Amount = uint64(math.Pow10(i)) // Populate data irrelevant to test - record.DataVersion = splitter_token.DataVersion1 record.Address = fmt.Sprintf("address%d", i) - record.Vault = fmt.Sprintf("vault%d", i) record.RecentRoot = fmt.Sprintf("root%d", i) record.Transcript = fmt.Sprintf("transcript%d", i) record.Destination = fmt.Sprintf("destination%d", i) @@ -380,25 +350,23 @@ func testCounts(t *testing.T, s commitment.Store) { t.Run("testCounts", func(t *testing.T) { ctx := context.Background() - futureVault1 := "future-vault-1" - futureVault2 := "future-vault-2" - futureVault3 := "future-vault-3" + futureCommitment1 := "future-commitment-1" + futureCommitment2 := "future-commitment-2" + futureCommitment3 := "future-commitment-3" records := []*commitment.Record{ - {State: commitment.StateReadyToOpen, RecentRoot: "root1", RepaymentDivertedTo: &futureVault1}, - {State: commitment.StateReadyToOpen, RecentRoot: "root1", RepaymentDivertedTo: &futureVault1}, - {State: commitment.StateReadyToOpen, RecentRoot: "root2", RepaymentDivertedTo: &futureVault2}, - {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureVault2}, - {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureVault2}, - {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureVault2}, + {State: commitment.StateReadyToOpen, RecentRoot: "root1", RepaymentDivertedTo: &futureCommitment1}, + {State: commitment.StateReadyToOpen, RecentRoot: "root1", RepaymentDivertedTo: &futureCommitment1}, + {State: commitment.StateReadyToOpen, RecentRoot: "root2", RepaymentDivertedTo: &futureCommitment2}, + {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureCommitment2, TreasuryRepaid: true}, + {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureCommitment2}, + {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureCommitment2}, } for i, record := range records { // Populate data irrelevant to test record.Pool = "pool" record.Amount = 1 - record.DataVersion = splitter_token.DataVersion1 record.Address = fmt.Sprintf("address%d", i) - record.Vault = fmt.Sprintf("vault%d", i) record.Transcript = fmt.Sprintf("transcript%d", i) record.Destination = fmt.Sprintf("destination%d", i) record.Intent = fmt.Sprintf("intent%d", i) @@ -421,32 +389,27 @@ func testCounts(t *testing.T, s commitment.Store) { require.NoError(t, err) assert.EqualValues(t, 3, count) - count, err = s.CountRepaymentsDivertedToVault(ctx, futureVault1) + count, err = s.CountPendingRepaymentsDivertedToCommitment(ctx, futureCommitment1) require.NoError(t, err) assert.EqualValues(t, 2, count) - count, err = s.CountRepaymentsDivertedToVault(ctx, futureVault2) + count, err = s.CountPendingRepaymentsDivertedToCommitment(ctx, futureCommitment2) require.NoError(t, err) - assert.EqualValues(t, 4, count) + assert.EqualValues(t, 3, count) - count, err = s.CountRepaymentsDivertedToVault(ctx, futureVault3) + count, err = s.CountPendingRepaymentsDivertedToCommitment(ctx, futureCommitment3) require.NoError(t, err) assert.EqualValues(t, 0, count) }) } func assertEquivalentRecords(t *testing.T, obj1, obj2 *commitment.Record) { - assert.Equal(t, obj1.DataVersion, obj2.DataVersion) assert.Equal(t, obj1.Address, obj2.Address) - assert.Equal(t, obj1.Bump, obj2.Bump) assert.Equal(t, obj1.Pool, obj2.Pool) - assert.Equal(t, obj1.PoolBump, obj2.PoolBump) assert.Equal(t, obj1.RecentRoot, obj2.RecentRoot) assert.Equal(t, obj1.Transcript, obj2.Transcript) assert.Equal(t, obj1.Destination, obj2.Destination) assert.Equal(t, obj1.Amount, obj2.Amount) - assert.Equal(t, obj1.Vault, obj2.Vault) - assert.Equal(t, obj1.VaultBump, obj2.VaultBump) assert.Equal(t, obj1.Intent, obj2.Intent) assert.Equal(t, obj1.ActionId, obj2.ActionId) assert.Equal(t, obj1.Owner, obj2.Owner) diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 5403156e..82285b37 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -317,14 +317,13 @@ type DatabaseData interface { // -------------------------------------------------------------------------------- SaveCommitment(ctx context.Context, record *commitment.Record) error GetCommitmentByAddress(ctx context.Context, address string) (*commitment.Record, error) - GetCommitmentByVault(ctx context.Context, vault string) (*commitment.Record, error) GetCommitmentByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) GetAllCommitmentsByState(ctx context.Context, state commitment.State, opts ...query.Option) ([]*commitment.Record, error) GetUpgradeableCommitmentsByOwner(ctx context.Context, owner string, limit uint64) ([]*commitment.Record, error) GetUsedTreasuryPoolDeficitFromCommitments(ctx context.Context, treasuryPool string) (uint64, error) GetTotalTreasuryPoolDeficitFromCommitments(ctx context.Context, treasuryPool string) (uint64, error) CountCommitmentsByState(ctx context.Context, state commitment.State) (uint64, error) - CountCommitmentRepaymentsDivertedToVault(ctx context.Context, vault string) (uint64, error) + CountPendingCommitmentRepaymentsDivertedToCommitment(ctx context.Context, address string) (uint64, error) // Treasury Pool // -------------------------------------------------------------------------------- @@ -1257,9 +1256,6 @@ func (dp *DatabaseProvider) SaveCommitment(ctx context.Context, record *commitme func (dp *DatabaseProvider) GetCommitmentByAddress(ctx context.Context, address string) (*commitment.Record, error) { return dp.commitment.GetByAddress(ctx, address) } -func (dp *DatabaseProvider) GetCommitmentByVault(ctx context.Context, vault string) (*commitment.Record, error) { - return dp.commitment.GetByVault(ctx, vault) -} func (dp *DatabaseProvider) GetCommitmentByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) { return dp.commitment.GetByAction(ctx, intentId, actionId) } @@ -1283,8 +1279,8 @@ func (dp *DatabaseProvider) GetTotalTreasuryPoolDeficitFromCommitments(ctx conte func (dp *DatabaseProvider) CountCommitmentsByState(ctx context.Context, state commitment.State) (uint64, error) { return dp.commitment.CountByState(ctx, state) } -func (dp *DatabaseProvider) CountCommitmentRepaymentsDivertedToVault(ctx context.Context, vault string) (uint64, error) { - return dp.commitment.CountRepaymentsDivertedToVault(ctx, vault) +func (dp *DatabaseProvider) CountPendingCommitmentRepaymentsDivertedToCommitment(ctx context.Context, address string) (uint64, error) { + return dp.commitment.CountPendingRepaymentsDivertedToCommitment(ctx, address) } // Treasury Pool diff --git a/pkg/solana/cvm/types_merkle_tree.go b/pkg/solana/cvm/types_merkle_tree.go index 1cb61e05..41313ec7 100644 --- a/pkg/solana/cvm/types_merkle_tree.go +++ b/pkg/solana/cvm/types_merkle_tree.go @@ -6,6 +6,10 @@ const ( MinMerkleTreeSize = 1 ) +var ( + MerkleTreePrefix = []byte("merkletree") +) + type MerkleTree struct { Root Hash Levels uint8 From 2093be2f06563ad9d85a1eba22674e49369a2753 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 24 Jul 2024 15:50:20 -0400 Subject: [PATCH 13/79] Update recent roots structure with new circular buffer structure (#156) --- pkg/solana/cvm/accounts_relay.go | 12 +++---- pkg/solana/cvm/types_recent_hashes.go | 41 ------------------------ pkg/solana/cvm/types_recent_roots.go | 45 +++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 47 deletions(-) delete mode 100644 pkg/solana/cvm/types_recent_hashes.go create mode 100644 pkg/solana/cvm/types_recent_roots.go diff --git a/pkg/solana/cvm/accounts_relay.go b/pkg/solana/cvm/accounts_relay.go index 9cd134cb..d174c4cc 100644 --- a/pkg/solana/cvm/accounts_relay.go +++ b/pkg/solana/cvm/accounts_relay.go @@ -32,9 +32,9 @@ type RelayAccount struct { NumLevels uint8 NumHistory uint8 - Treasury TokenPool - History MerkleTree - RecentHashes RecentHashes + Treasury TokenPool + History MerkleTree + RecentRoots RecentRoots } func (obj *RelayAccount) Unmarshal(data []byte) error { @@ -62,7 +62,7 @@ func (obj *RelayAccount) Unmarshal(data []byte) error { getTokenPool(data, &obj.Treasury, &offset) getMerkleTree(data, &obj.History, &offset) - getRecentHashes(data, &obj.RecentHashes, &offset) + getRecentRoots(data, &obj.RecentRoots, &offset) return nil } @@ -77,12 +77,12 @@ func (obj *RelayAccount) String() string { obj.NumHistory, obj.Treasury.String(), obj.History.String(), - obj.RecentHashes.String(), + obj.RecentRoots.String(), ) } func GetRelayAccountSize(numLevels, numHistory int) int { return (minRelayAccountSize + +GetMerkleTreeSize(numLevels) + // history - +GetRecentHashesSize(numHistory)) // recent_hashes + +GetRecentRootsSize(numHistory)) // recent_roots } diff --git a/pkg/solana/cvm/types_recent_hashes.go b/pkg/solana/cvm/types_recent_hashes.go deleted file mode 100644 index dee0b43b..00000000 --- a/pkg/solana/cvm/types_recent_hashes.go +++ /dev/null @@ -1,41 +0,0 @@ -package cvm - -import ( - "fmt" -) - -type RecentHashes struct { - CurrentIndex uint8 - Items HashArray -} - -func (obj *RecentHashes) Unmarshal(data []byte) error { - if len(data) < 1 { - return ErrInvalidAccountData - } - - var offset int - - getUint8(data, &obj.CurrentIndex, &offset) - getHashArray(data, &obj.Items, &offset) - - return nil -} - -func (obj *RecentHashes) String() string { - return fmt.Sprintf( - "RecentHashes{current_index=%d,items=%s}", - obj.CurrentIndex, - obj.Items.String(), - ) -} - -func getRecentHashes(src []byte, dst *RecentHashes, offset *int) { - dst.Unmarshal(src[*offset:]) - *offset += GetRecentHashesSize(len(dst.Items)) -} - -func GetRecentHashesSize(numItems int) int { - return (1 + // current_index - 4 + numItems*HashSize) // items -} diff --git a/pkg/solana/cvm/types_recent_roots.go b/pkg/solana/cvm/types_recent_roots.go new file mode 100644 index 00000000..19b749b6 --- /dev/null +++ b/pkg/solana/cvm/types_recent_roots.go @@ -0,0 +1,45 @@ +package cvm + +import ( + "fmt" +) + +type RecentRoots struct { + Capacity uint8 + Offset uint8 + Items HashArray +} + +func (obj *RecentRoots) Unmarshal(data []byte) error { + if len(data) < 1 { + return ErrInvalidAccountData + } + + var offset int + + getUint8(data, &obj.Capacity, &offset) + getUint8(data, &obj.Offset, &offset) + getHashArray(data, &obj.Items, &offset) + + return nil +} + +func (obj *RecentRoots) String() string { + return fmt.Sprintf( + "RecentRoots{capacity=%d,offset=%d,items=%s}", + obj.Capacity, + obj.Offset, + obj.Items.String(), + ) +} + +func getRecentRoots(src []byte, dst *RecentRoots, offset *int) { + dst.Unmarshal(src[*offset:]) + *offset += GetRecentRootsSize(len(dst.Items)) +} + +func GetRecentRootsSize(numItems int) int { + return (1 + // capacity + 1 + // offset + 4 + numItems*HashSize) // items +} From 7afc626039a6f3780a67e509c08db7eefc65115a Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 25 Jul 2024 14:14:05 -0400 Subject: [PATCH 14/79] VM treasury worker (#157) * Update treasury pool store with VM related changes * Fix commitment worker tests * Update treasury worker to use new VM state --- pkg/code/async/commitment/testutil.go | 3 -- pkg/code/async/treasury/testutil.go | 2 -- pkg/code/async/treasury/worker.go | 10 ++---- pkg/code/data/treasury/postgres/model.go | 30 ++++++---------- pkg/code/data/treasury/postgres/store_test.go | 2 -- pkg/code/data/treasury/tests/tests.go | 17 ++++------ pkg/code/data/treasury/treasury.go | 34 ++++++------------- pkg/solana/cvm/accounts_relay.go | 2 +- 8 files changed, 31 insertions(+), 69 deletions(-) diff --git a/pkg/code/async/commitment/testutil.go b/pkg/code/async/commitment/testutil.go index c1d83388..33abe4a9 100644 --- a/pkg/code/async/commitment/testutil.go +++ b/pkg/code/async/commitment/testutil.go @@ -23,7 +23,6 @@ import ( "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/solana/cvm" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/testutil" ) @@ -48,8 +47,6 @@ func setup(t *testing.T) testEnv { treasuryPoolAddress := testutil.NewRandomAccount(t) treasuryPool := &treasury.Record{ - DataVersion: splitter_token.DataVersion1, - Name: "test-pool", Address: treasuryPoolAddress.PublicKey().ToBase58(), diff --git a/pkg/code/async/treasury/testutil.go b/pkg/code/async/treasury/testutil.go index 6cb6beff..6b4d8444 100644 --- a/pkg/code/async/treasury/testutil.go +++ b/pkg/code/async/treasury/testutil.go @@ -52,8 +52,6 @@ func setup(t *testing.T, testOverrides *testOverrides) *testEnv { treasuryPoolAddress := testutil.NewRandomAccount(t) treasuryPool := &treasury.Record{ - DataVersion: splitter_token.DataVersion1, - Name: "test-pool", Address: treasuryPoolAddress.PublicKey().ToBase58(), diff --git a/pkg/code/async/treasury/worker.go b/pkg/code/async/treasury/worker.go index e9f1db29..1ef8ca0b 100644 --- a/pkg/code/async/treasury/worker.go +++ b/pkg/code/async/treasury/worker.go @@ -8,11 +8,11 @@ import ( "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkg/errors" + "github.com/code-payments/code-server/pkg/code/data/treasury" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/retry" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/code/data/treasury" + "github.com/code-payments/code-server/pkg/solana/cvm" ) const ( @@ -106,17 +106,13 @@ func (p *service) handleAvailable(ctx context.Context, record *treasury.Record) } func (p *service) updateAccountState(ctx context.Context, record *treasury.Record) error { - if record.DataVersion != splitter_token.DataVersion1 { - return errors.New("unsupported data version") - } - // todo: Use a smarter block. Perhaps from the last finalized payment? data, solanaBlock, err := p.data.GetBlockchainAccountDataAfterBlock(ctx, record.Address, record.SolanaBlock) if err != nil { return errors.Wrap(err, "error querying latest account data from blockchain") } - var unmarshalled splitter_token.PoolAccount + var unmarshalled cvm.RelayAccount err = unmarshalled.Unmarshal(data) if err != nil { return errors.Wrap(err, "error unmarshalling account data") diff --git a/pkg/code/data/treasury/postgres/model.go b/pkg/code/data/treasury/postgres/model.go index adb3b0b9..5b26597a 100644 --- a/pkg/code/data/treasury/postgres/model.go +++ b/pkg/code/data/treasury/postgres/model.go @@ -8,10 +8,9 @@ import ( "github.com/jmoiron/sqlx" + "github.com/code-payments/code-server/pkg/code/data/treasury" pgutil "github.com/code-payments/code-server/pkg/database/postgres" q "github.com/code-payments/code-server/pkg/database/query" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/code/data/treasury" ) const ( @@ -23,8 +22,6 @@ const ( type treasuryPoolModel struct { Id sql.NullInt64 `db:"id"` - DataVersion uint `db:"data_version"` - Name string `db:"name"` Address string `db:"address"` @@ -81,8 +78,6 @@ func toTreasuryPoolModel(obj *treasury.Record) (*treasuryPoolModel, error) { } return &treasuryPoolModel{ - DataVersion: uint(obj.DataVersion), - Name: obj.Name, Address: obj.Address, @@ -116,8 +111,6 @@ func fromTreasuryPoolModel(obj *treasuryPoolModel) *treasury.Record { return &treasury.Record{ Id: uint64(obj.Id.Int64), - DataVersion: splitter_token.DataVersion(obj.DataVersion), - Name: obj.Name, Address: obj.Address, @@ -147,18 +140,17 @@ func (m *treasuryPoolModel) dbSave(ctx context.Context, db *sqlx.DB) error { m.LastUpdatedAt = time.Now() query := `INSERT INTO ` + treasuryPoolTableName + ` - (data_version, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + (name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (address) DO UPDATE - SET current_index = $9, solana_block = $11, last_updated_at = $13 - WHERE ` + treasuryPoolTableName + `.address = $3 AND ` + treasuryPoolTableName + `.vault = $5 AND ` + treasuryPoolTableName + `.solana_block < $11 - RETURNING id, data_version, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at + SET current_index = $8, solana_block = $10, last_updated_at = $12 + WHERE ` + treasuryPoolTableName + `.address = $2 AND ` + treasuryPoolTableName + `.vault = $4 AND ` + treasuryPoolTableName + `.solana_block < $10 + RETURNING id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at ` err := tx.QueryRowxContext( ctx, query, - m.DataVersion, m.Name, m.Address, m.Bump, @@ -178,7 +170,7 @@ func (m *treasuryPoolModel) dbSave(ctx context.Context, db *sqlx.DB) error { query = `INSERT INTO ` + recentRootTableName + ` (pool, index, recent_root, at_solana_block) - VALUES ($1,$2,$3, $4) + VALUES ($1,$2,$3,$4) RETURNING id, pool, index, recent_root, at_solana_block ` for _, historyItem := range m.HistoryList { @@ -265,7 +257,7 @@ func (m *fundingModel) dbSave(ctx context.Context, db *sqlx.DB) error { func dbGetByName(ctx context.Context, db *sqlx.DB, name string) (*treasuryPoolModel, error) { var res treasuryPoolModel - query := `SELECT id, data_version, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` + query := `SELECT id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` WHERE name = $1 ` err := db.GetContext(ctx, &res, query, name) @@ -283,7 +275,7 @@ func dbGetByName(ctx context.Context, db *sqlx.DB, name string) (*treasuryPoolMo func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*treasuryPoolModel, error) { var res treasuryPoolModel - query := `SELECT id, data_version, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` + query := `SELECT id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` WHERE address = $1 ` err := db.GetContext(ctx, &res, query, address) @@ -301,7 +293,7 @@ func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*treasury func dbGetByVault(ctx context.Context, db *sqlx.DB, vault string) (*treasuryPoolModel, error) { var res treasuryPoolModel - query := `SELECT id, data_version, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` + query := `SELECT id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` WHERE vault = $1 ` err := db.GetContext(ctx, &res, query, vault) @@ -320,7 +312,7 @@ func dbGetByVault(ctx context.Context, db *sqlx.DB, vault string) (*treasuryPool func dbGetAllByState(ctx context.Context, db *sqlx.DB, state treasury.TreasuryPoolState, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*treasuryPoolModel, error) { res := []*treasuryPoolModel{} - query := `SELECT id, data_version, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at + query := `SELECT id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` WHERE (state = $1) ` diff --git a/pkg/code/data/treasury/postgres/store_test.go b/pkg/code/data/treasury/postgres/store_test.go index ef05f523..f0274ee8 100644 --- a/pkg/code/data/treasury/postgres/store_test.go +++ b/pkg/code/data/treasury/postgres/store_test.go @@ -22,8 +22,6 @@ const ( CREATE TABLE codewallet__core_treasurypool( id SERIAL NOT NULL PRIMARY KEY, - data_version INTEGER NOT NULL, - name TEXT NOT NULL, address TEXT NOT NULL, diff --git a/pkg/code/data/treasury/tests/tests.go b/pkg/code/data/treasury/tests/tests.go index 1694e053..91a8eb70 100644 --- a/pkg/code/data/treasury/tests/tests.go +++ b/pkg/code/data/treasury/tests/tests.go @@ -8,9 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/database/query" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" "github.com/code-payments/code-server/pkg/code/data/treasury" + "github.com/code-payments/code-server/pkg/database/query" ) func RunTests(t *testing.T, s treasury.Store, teardown func()) { @@ -31,8 +30,6 @@ func testTreasuryPoolHappyPath(t *testing.T, s treasury.Store) { start := time.Now() expected := &treasury.Record{ - DataVersion: splitter_token.DataVersion1, - Name: "name", Address: "treasury", @@ -134,11 +131,11 @@ func testGetAllByState(t *testing.T, s treasury.Store) { assert.Equal(t, treasury.ErrTreasuryPoolNotFound, err) expected := []*treasury.Record{ - {DataVersion: splitter_token.DataVersion1, Name: "name1", Address: "treasury1", Vault: "vault1", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root1"}, SolanaBlock: 1, State: treasury.TreasuryPoolStateAvailable}, - {DataVersion: splitter_token.DataVersion1, Name: "name2", Address: "treasury2", Vault: "vault2", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root2"}, SolanaBlock: 2, State: treasury.TreasuryPoolStateAvailable}, - {DataVersion: splitter_token.DataVersion1, Name: "name3", Address: "treasury3", Vault: "vault3", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root3"}, SolanaBlock: 3, State: treasury.TreasuryPoolStateAvailable}, - {DataVersion: splitter_token.DataVersion1, Name: "name4", Address: "treasury4", Vault: "vault4", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root4"}, SolanaBlock: 4, State: treasury.TreasuryPoolStateDeprecated}, - {DataVersion: splitter_token.DataVersion1, Name: "name5", Address: "treasury5", Vault: "vault5", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root5"}, SolanaBlock: 5, State: treasury.TreasuryPoolStateDeprecated}, + {Name: "name1", Address: "treasury1", Vault: "vault1", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root1"}, SolanaBlock: 1, State: treasury.TreasuryPoolStateAvailable}, + {Name: "name2", Address: "treasury2", Vault: "vault2", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root2"}, SolanaBlock: 2, State: treasury.TreasuryPoolStateAvailable}, + {Name: "name3", Address: "treasury3", Vault: "vault3", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root3"}, SolanaBlock: 3, State: treasury.TreasuryPoolStateAvailable}, + {Name: "name4", Address: "treasury4", Vault: "vault4", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root4"}, SolanaBlock: 4, State: treasury.TreasuryPoolStateDeprecated}, + {Name: "name5", Address: "treasury5", Vault: "vault5", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root5"}, SolanaBlock: 5, State: treasury.TreasuryPoolStateDeprecated}, } for _, record := range expected { require.NoError(t, s.Save(ctx, record)) @@ -242,8 +239,6 @@ func testFundingHappyPath(t *testing.T, s treasury.Store) { } func assertEquivalentTreasuryPoolRecords(t *testing.T, obj1, obj2 *treasury.Record) { - assert.Equal(t, obj1.DataVersion, obj2.DataVersion) - assert.Equal(t, obj1.Name, obj2.Name) assert.Equal(t, obj1.Address, obj2.Address) diff --git a/pkg/code/data/treasury/treasury.go b/pkg/code/data/treasury/treasury.go index 7e65928c..661b26e4 100644 --- a/pkg/code/data/treasury/treasury.go +++ b/pkg/code/data/treasury/treasury.go @@ -7,7 +7,7 @@ import ( "github.com/mr-tron/base58" "github.com/pkg/errors" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" + "github.com/code-payments/code-server/pkg/solana/cvm" ) type TreasuryPoolState uint8 @@ -29,8 +29,6 @@ const ( type Record struct { Id uint64 - DataVersion splitter_token.DataVersion - Name string Address string @@ -90,11 +88,7 @@ func (r *Record) ContainsRecentRoot(recentRoot string) (bool, int) { return false, 0 } -func (r *Record) Update(data *splitter_token.PoolAccount, solanaBlock uint64) error { - if data.DataVersion != splitter_token.DataVersion1 { - return errors.New("data version must be 1") - } - +func (r *Record) Update(data *cvm.RelayAccount, solanaBlock uint64) error { // Sanity check we're updating the right record by computing and checking // the expected vault address @@ -103,14 +97,14 @@ func (r *Record) Update(data *splitter_token.PoolAccount, solanaBlock uint64) er return errors.Wrap(err, "error decoding address") } - vaultAddressBytes, _, err := splitter_token.GetPoolVaultAddress(&splitter_token.GetPoolVaultAddressArgs{ - Pool: addressBytes, + vaultAddressBytes, _, err := cvm.GetRelayVaultAddress(&cvm.GetRelayVaultAddressArgs{ + RelayOrProof: addressBytes, }) if err != nil { return errors.Wrap(err, "error getting vault address") } - if !bytes.Equal(vaultAddressBytes, data.Vault) { + if !bytes.Equal(vaultAddressBytes, data.Treasury.Vault) { return errors.New("updating wrong pool record") } @@ -120,10 +114,10 @@ func (r *Record) Update(data *splitter_token.PoolAccount, solanaBlock uint64) er return ErrStaleTreasuryPoolState } - if r.CurrentIndex == data.CurrentIndex { + if r.CurrentIndex == data.RecentRoots.Offset { var hasUpdatedHistoryList bool for i := 0; i < int(r.HistoryListSize); i++ { - if r.HistoryList[i] != data.HistoryList[i].ToString() { + if r.HistoryList[i] != data.RecentRoots.Items[i].String() { hasUpdatedHistoryList = true break } @@ -136,11 +130,11 @@ func (r *Record) Update(data *splitter_token.PoolAccount, solanaBlock uint64) er // It's now safe to update the record - r.CurrentIndex = data.CurrentIndex + r.CurrentIndex = data.RecentRoots.Offset historyList := make([]string, r.HistoryListSize) - for i, recentRoot := range data.HistoryList { - historyList[i] = recentRoot.ToString() + for i, recentRoot := range data.RecentRoots.Items { + historyList[i] = recentRoot.String() } r.HistoryList = historyList @@ -150,10 +144,6 @@ func (r *Record) Update(data *splitter_token.PoolAccount, solanaBlock uint64) er } func (r *Record) Validate() error { - if r.DataVersion != splitter_token.DataVersion1 { - return errors.New("data version must be 1") - } - if len(r.Name) == 0 { return errors.New("name is required") } @@ -202,8 +192,6 @@ func (r *Record) Clone() *Record { return &Record{ Id: r.Id, - DataVersion: r.DataVersion, - Name: r.Name, Address: r.Address, @@ -231,8 +219,6 @@ func (r *Record) Clone() *Record { func (r *Record) CopyTo(dst *Record) { dst.Id = r.Id - dst.DataVersion = r.DataVersion - dst.Name = r.Name dst.Address = r.Address diff --git a/pkg/solana/cvm/accounts_relay.go b/pkg/solana/cvm/accounts_relay.go index d174c4cc..b87a6ce4 100644 --- a/pkg/solana/cvm/accounts_relay.go +++ b/pkg/solana/cvm/accounts_relay.go @@ -69,7 +69,7 @@ func (obj *RelayAccount) Unmarshal(data []byte) error { func (obj *RelayAccount) String() string { return fmt.Sprintf( - "RelayAccount{vm=%s,bump=%d,name=%s,num_levels=%d,num_history=%d,treasury=%s,history=%s,recent_hashes=%s}", + "RelayAccount{vm=%s,bump=%d,name=%s,num_levels=%d,num_history=%d,treasury=%s,history=%s,recent_roots=%s}", base58.Encode(obj.Vm), obj.Bump, obj.Name, From 83ba381cc613fd342e1a9bcf76536857dfdaa021 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 29 Jul 2024 09:38:33 -0400 Subject: [PATCH 15/79] Update treasury worker to use new save recent root instruction (#158) --- pkg/code/async/commitment/testutil.go | 2 ++ pkg/code/async/treasury/recent_root.go | 23 +++++++++-------- pkg/code/async/treasury/testutil.go | 7 ++++-- pkg/code/data/treasury/postgres/model.go | 25 ++++++++++++------- pkg/code/data/treasury/postgres/store_test.go | 2 ++ pkg/code/data/treasury/tests/tests.go | 12 +++++---- pkg/code/data/treasury/treasury.go | 10 ++++++++ 7 files changed, 55 insertions(+), 26 deletions(-) diff --git a/pkg/code/async/commitment/testutil.go b/pkg/code/async/commitment/testutil.go index 33abe4a9..ae1b5c72 100644 --- a/pkg/code/async/commitment/testutil.go +++ b/pkg/code/async/commitment/testutil.go @@ -47,6 +47,8 @@ func setup(t *testing.T) testEnv { treasuryPoolAddress := testutil.NewRandomAccount(t) treasuryPool := &treasury.Record{ + Vm: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + Name: "test-pool", Address: treasuryPoolAddress.PublicKey().ToBase58(), diff --git a/pkg/code/async/treasury/recent_root.go b/pkg/code/async/treasury/recent_root.go index 52b44879..0d9b8f4a 100644 --- a/pkg/code/async/treasury/recent_root.go +++ b/pkg/code/async/treasury/recent_root.go @@ -21,7 +21,7 @@ import ( "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/solana" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" + "github.com/code-payments/code-server/pkg/solana/cvm" ) // This method is expected to be extremely safe due to the implications of saving @@ -299,21 +299,24 @@ func (p *service) openTreasuryAdvanceFloodGates(ctx context.Context, treasuryPoo } func makeSaveRecentRootTransaction(selectedNonce *transaction.SelectedNonce, record *treasury.Record) (solana.Transaction, error) { + vmAddressBytes, err := base58.Decode(record.Vm) + if err != nil { + return solana.Transaction{}, err + } + treasuryAddressBytes, err := base58.Decode(record.Address) if err != nil { return solana.Transaction{}, err } - saveRecentRootInstruction := splitter_token.NewSaveRecentRootInstruction( - &splitter_token.SaveRecentRootInstructionAccounts{ - Pool: treasuryAddressBytes, - Authority: common.GetSubsidizer().PublicKey().ToBytes(), - Payer: common.GetSubsidizer().PublicKey().ToBytes(), - }, - &splitter_token.SaveRecentRootInstructionArgs{ - PoolBump: record.Bump, + saveRecentRootInstruction := cvm.NewRelaySaveRecentRootInstruction( + &cvm.RelaySaveRecentRootInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vmAddressBytes, + Relay: treasuryAddressBytes, }, - ).ToLegacyInstruction() + &cvm.RelaySaveRecentRootInstructionArgs{}, + ) // Always use a nonce for this type of transaction. It's way too risky without it, // given the implications if we play this out too many times by accident. diff --git a/pkg/code/async/treasury/testutil.go b/pkg/code/async/treasury/testutil.go index 6b4d8444..55a826f0 100644 --- a/pkg/code/async/treasury/testutil.go +++ b/pkg/code/async/treasury/testutil.go @@ -52,6 +52,8 @@ func setup(t *testing.T, testOverrides *testOverrides) *testEnv { treasuryPoolAddress := testutil.NewRandomAccount(t) treasuryPool := &treasury.Record{ + Vm: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + Name: "test-pool", Address: treasuryPoolAddress.PublicKey().ToBase58(), @@ -376,12 +378,13 @@ func (e *testEnv) assertIntentCreated(t *testing.T) { assert.Equal(t, *fulfillmentRecord.Nonce, base58.Encode(advanceNonceIxn.Nonce)) assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), base58.Encode(advanceNonceIxn.Authority)) - saveRecentRootIxnArgs, saveRecentRootIxnAccounts, err := splitter_token.SaveRecentRootInstructionFromLegacyInstruction(txn, 1) + // todo: Ability to decompile CVM save recent root ixn (legacy code in comments) + /*saveRecentRootIxnArgs, saveRecentRootIxnAccounts, err := splitter_token.SaveRecentRootInstructionFromLegacyInstruction(txn, 1) require.NoError(t, err) assert.Equal(t, e.treasuryPool.Bump, saveRecentRootIxnArgs.PoolBump) assert.Equal(t, e.treasuryPool.Address, base58.Encode(saveRecentRootIxnAccounts.Pool)) assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), base58.Encode(saveRecentRootIxnAccounts.Authority)) - assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), base58.Encode(saveRecentRootIxnAccounts.Payer)) + assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), base58.Encode(saveRecentRootIxnAccounts.Payer))*/ nonceRecord, err := e.data.GetNonce(e.ctx, *fulfillmentRecord.Nonce) require.NoError(t, err) diff --git a/pkg/code/data/treasury/postgres/model.go b/pkg/code/data/treasury/postgres/model.go index 5b26597a..4b913f42 100644 --- a/pkg/code/data/treasury/postgres/model.go +++ b/pkg/code/data/treasury/postgres/model.go @@ -22,6 +22,8 @@ const ( type treasuryPoolModel struct { Id sql.NullInt64 `db:"id"` + Vm string `db:"vm"` + Name string `db:"name"` Address string `db:"address"` @@ -78,6 +80,8 @@ func toTreasuryPoolModel(obj *treasury.Record) (*treasuryPoolModel, error) { } return &treasuryPoolModel{ + Vm: obj.Vm, + Name: obj.Name, Address: obj.Address, @@ -111,6 +115,8 @@ func fromTreasuryPoolModel(obj *treasuryPoolModel) *treasury.Record { return &treasury.Record{ Id: uint64(obj.Id.Int64), + Vm: obj.Vm, + Name: obj.Name, Address: obj.Address, @@ -140,17 +146,18 @@ func (m *treasuryPoolModel) dbSave(ctx context.Context, db *sqlx.DB) error { m.LastUpdatedAt = time.Now() query := `INSERT INTO ` + treasuryPoolTableName + ` - (name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + (vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ON CONFLICT (address) DO UPDATE - SET current_index = $8, solana_block = $10, last_updated_at = $12 - WHERE ` + treasuryPoolTableName + `.address = $2 AND ` + treasuryPoolTableName + `.vault = $4 AND ` + treasuryPoolTableName + `.solana_block < $10 - RETURNING id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at + SET current_index = $9, solana_block = $11, last_updated_at = $13 + WHERE ` + treasuryPoolTableName + `.address = $3 AND ` + treasuryPoolTableName + `.vault = $5 AND ` + treasuryPoolTableName + `.solana_block < $11 + RETURNING id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at ` err := tx.QueryRowxContext( ctx, query, + m.Vm, m.Name, m.Address, m.Bump, @@ -257,7 +264,7 @@ func (m *fundingModel) dbSave(ctx context.Context, db *sqlx.DB) error { func dbGetByName(ctx context.Context, db *sqlx.DB, name string) (*treasuryPoolModel, error) { var res treasuryPoolModel - query := `SELECT id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` + query := `SELECT id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` WHERE name = $1 ` err := db.GetContext(ctx, &res, query, name) @@ -275,7 +282,7 @@ func dbGetByName(ctx context.Context, db *sqlx.DB, name string) (*treasuryPoolMo func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*treasuryPoolModel, error) { var res treasuryPoolModel - query := `SELECT id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` + query := `SELECT id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` WHERE address = $1 ` err := db.GetContext(ctx, &res, query, address) @@ -293,7 +300,7 @@ func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*treasury func dbGetByVault(ctx context.Context, db *sqlx.DB, vault string) (*treasuryPoolModel, error) { var res treasuryPoolModel - query := `SELECT id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` + query := `SELECT id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` WHERE vault = $1 ` err := db.GetContext(ctx, &res, query, vault) @@ -312,7 +319,7 @@ func dbGetByVault(ctx context.Context, db *sqlx.DB, vault string) (*treasuryPool func dbGetAllByState(ctx context.Context, db *sqlx.DB, state treasury.TreasuryPoolState, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*treasuryPoolModel, error) { res := []*treasuryPoolModel{} - query := `SELECT id, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at + query := `SELECT id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` WHERE (state = $1) ` diff --git a/pkg/code/data/treasury/postgres/store_test.go b/pkg/code/data/treasury/postgres/store_test.go index f0274ee8..aca5cca6 100644 --- a/pkg/code/data/treasury/postgres/store_test.go +++ b/pkg/code/data/treasury/postgres/store_test.go @@ -22,6 +22,8 @@ const ( CREATE TABLE codewallet__core_treasurypool( id SERIAL NOT NULL PRIMARY KEY, + vm TEXT NOT NULL, + name TEXT NOT NULL, address TEXT NOT NULL, diff --git a/pkg/code/data/treasury/tests/tests.go b/pkg/code/data/treasury/tests/tests.go index 91a8eb70..b89793c7 100644 --- a/pkg/code/data/treasury/tests/tests.go +++ b/pkg/code/data/treasury/tests/tests.go @@ -30,6 +30,8 @@ func testTreasuryPoolHappyPath(t *testing.T, s treasury.Store) { start := time.Now() expected := &treasury.Record{ + Vm: "vm", + Name: "name", Address: "treasury", @@ -131,11 +133,11 @@ func testGetAllByState(t *testing.T, s treasury.Store) { assert.Equal(t, treasury.ErrTreasuryPoolNotFound, err) expected := []*treasury.Record{ - {Name: "name1", Address: "treasury1", Vault: "vault1", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root1"}, SolanaBlock: 1, State: treasury.TreasuryPoolStateAvailable}, - {Name: "name2", Address: "treasury2", Vault: "vault2", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root2"}, SolanaBlock: 2, State: treasury.TreasuryPoolStateAvailable}, - {Name: "name3", Address: "treasury3", Vault: "vault3", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root3"}, SolanaBlock: 3, State: treasury.TreasuryPoolStateAvailable}, - {Name: "name4", Address: "treasury4", Vault: "vault4", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root4"}, SolanaBlock: 4, State: treasury.TreasuryPoolStateDeprecated}, - {Name: "name5", Address: "treasury5", Vault: "vault5", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root5"}, SolanaBlock: 5, State: treasury.TreasuryPoolStateDeprecated}, + {Vm: "vm", Name: "name1", Address: "treasury1", Vault: "vault1", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root1"}, SolanaBlock: 1, State: treasury.TreasuryPoolStateAvailable}, + {Vm: "vm", Name: "name2", Address: "treasury2", Vault: "vault2", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root2"}, SolanaBlock: 2, State: treasury.TreasuryPoolStateAvailable}, + {Vm: "vm", Name: "name3", Address: "treasury3", Vault: "vault3", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root3"}, SolanaBlock: 3, State: treasury.TreasuryPoolStateAvailable}, + {Vm: "vm", Name: "name4", Address: "treasury4", Vault: "vault4", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root4"}, SolanaBlock: 4, State: treasury.TreasuryPoolStateDeprecated}, + {Vm: "vm", Name: "name5", Address: "treasury5", Vault: "vault5", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root5"}, SolanaBlock: 5, State: treasury.TreasuryPoolStateDeprecated}, } for _, record := range expected { require.NoError(t, s.Save(ctx, record)) diff --git a/pkg/code/data/treasury/treasury.go b/pkg/code/data/treasury/treasury.go index 661b26e4..b0d72d35 100644 --- a/pkg/code/data/treasury/treasury.go +++ b/pkg/code/data/treasury/treasury.go @@ -29,6 +29,8 @@ const ( type Record struct { Id uint64 + Vm string + Name string Address string @@ -144,6 +146,10 @@ func (r *Record) Update(data *cvm.RelayAccount, solanaBlock uint64) error { } func (r *Record) Validate() error { + if len(r.Vm) == 0 { + return errors.New("vm is required") + } + if len(r.Name) == 0 { return errors.New("name is required") } @@ -192,6 +198,8 @@ func (r *Record) Clone() *Record { return &Record{ Id: r.Id, + Vm: r.Vm, + Name: r.Name, Address: r.Address, @@ -219,6 +227,8 @@ func (r *Record) Clone() *Record { func (r *Record) CopyTo(dst *Record) { dst.Id = r.Id + dst.Vm = r.Vm + dst.Name = r.Name dst.Address = r.Address From cfb19ef62f8bde91ed3dd5a4bb212e95888ef691 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Tue, 30 Jul 2024 09:28:09 -0400 Subject: [PATCH 16/79] First Timelock DB model update (#160) * Simplify Timelock DB model in new VM world * Update core common and balance packages * Fix several worker tests --- pkg/code/async/account/testutil.go | 3 +- pkg/code/async/commitment/testutil.go | 2 +- pkg/code/balance/calculator.go | 51 +- pkg/code/balance/calculator_test.go | 631 +++++++----------- pkg/code/common/account.go | 443 +++--------- pkg/code/common/account_test.go | 493 +------------- pkg/code/common/owner.go | 64 +- pkg/code/common/owner_test.go | 8 +- pkg/code/data/timelock/memory/store.go | 7 +- pkg/code/data/timelock/postgres/model.go | 56 +- pkg/code/data/timelock/postgres/store_test.go | 8 - pkg/code/data/timelock/tests/tests.go | 118 ---- pkg/code/data/timelock/timelock.go | 148 +--- 13 files changed, 407 insertions(+), 1625 deletions(-) diff --git a/pkg/code/async/account/testutil.go b/pkg/code/async/account/testutil.go index a2401364..b740f5b8 100644 --- a/pkg/code/async/account/testutil.go +++ b/pkg/code/async/account/testutil.go @@ -22,7 +22,6 @@ import ( "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" memory_push "github.com/code-payments/code-server/pkg/push/memory" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/testutil" ) @@ -63,7 +62,7 @@ func setup(t *testing.T) *testEnv { func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *testGiftCard { authority := testutil.NewRandomAccount(t) - timelockAccounts, err := authority.GetTimelockAccounts(timelock_token.DataVersion1, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(common.KinMintAccount) require.NoError(t, err) accountInfoRecord := &account.Record{ diff --git a/pkg/code/async/commitment/testutil.go b/pkg/code/async/commitment/testutil.go index ae1b5c72..b90da2fa 100644 --- a/pkg/code/async/commitment/testutil.go +++ b/pkg/code/async/commitment/testutil.go @@ -122,7 +122,7 @@ func (e testEnv) simulateCommitment(t *testing.T, recentRoot string, state commi require.NoError(t, e.data.SaveCommitment(e.ctx, commitmentRecord)) owner := testutil.NewRandomAccount(t) - timelockAccounts, err := owner.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(common.KinMintAccount) require.NoError(t, err) intentRecord := &intent.Record{ diff --git a/pkg/code/balance/calculator.go b/pkg/code/balance/calculator.go index 001b3ded..c8c882ab 100644 --- a/pkg/code/balance/calculator.go +++ b/pkg/code/balance/calculator.go @@ -15,7 +15,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/solana" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) type Source uint8 @@ -97,12 +96,6 @@ func CalculateFromCache(ctx context.Context, data code_data.Provider, tokenAccou FundingFromExternalDeposits(ctx, data), NetBalanceFromIntentActions(ctx, data), } - if timelockRecord.DataVersion == timelock_token.DataVersionLegacy { - strategies = []Strategy{ - FundingFromExternalDepositsForPrePrivacy2022Accounts(ctx, data), - NetBalanceFromPrePrivacy2022Intents(ctx, data), - } - } balance, err := Calculate( ctx, @@ -219,24 +212,6 @@ func NetBalanceFromIntentActions(ctx context.Context, data code_data.Provider) S } } -func NetBalanceFromPrePrivacy2022Intents(ctx context.Context, data code_data.Provider) Strategy { - return func(ctx context.Context, tokenAccount *common.Account, state *State) (*State, error) { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "NetBalanceFromPrePrivacy2022Intents", - "account": tokenAccount.PublicKey().ToBase58(), - }) - - netBalance, err := data.GetNetBalanceFromPrePrivacy2022Intents(ctx, tokenAccount.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting net balance from pre-privacy intents") - return nil, errors.Wrap(err, "error getting net balance from pre-privacy intents") - } - - state.current += netBalance - return state, nil - } -} - // FundingFromExternalDeposits is a balance calculation strategy that adds funding // from deposits from external accounts. func FundingFromExternalDeposits(ctx context.Context, data code_data.Provider) Strategy { @@ -257,24 +232,6 @@ func FundingFromExternalDeposits(ctx context.Context, data code_data.Provider) S } } -func FundingFromExternalDepositsForPrePrivacy2022Accounts(ctx context.Context, data code_data.Provider) Strategy { - return func(ctx context.Context, tokenAccount *common.Account, state *State) (*State, error) { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "FundingFromLegacyExternalDeposits", - "account": tokenAccount.PublicKey().ToBase58(), - }) - - amount, err := data.GetLegacyTotalExternalDepositAmountFromPrePrivacy2022Accounts(ctx, tokenAccount.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting external deposit amount") - return nil, errors.Wrap(err, "error getting external deposit amount") - } - state.current += int64(amount) - - return state, nil - } -} - // BatchCalculator is a functiona that calculates a batch of accounts' balances type BatchCalculator func(ctx context.Context, data code_data.Provider, accountRecordsBatch []*common.AccountRecords) (map[string]uint64, error) @@ -367,13 +324,7 @@ func defaultBatchCalculationFromCache(ctx context.Context, data code_data.Provid return nil, ErrNotManagedByCode } - // We only support post-privacy accounts - switch timelockRecord.DataVersion { - case timelock_token.DataVersion1: - tokenAccounts = append(tokenAccounts, timelockRecord.VaultAddress) - default: - return nil, ErrUnhandledAccount - } + tokenAccounts = append(tokenAccounts, timelockRecord.VaultAddress) } return CalculateBatch( diff --git a/pkg/code/balance/calculator_test.go b/pkg/code/balance/calculator_test.go index 6f9153af..1e4ec1e0 100644 --- a/pkg/code/balance/calculator_test.go +++ b/pkg/code/balance/calculator_test.go @@ -30,14 +30,14 @@ func TestDefaultCalculationMethods_NewCodeAccount(t *testing.T) { env := setupBalanceTestEnv(t) newOwnerAccount := testutil.NewRandomAccount(t) - newTokenAccount, err := newOwnerAccount.ToTimelockVault(getTimelockDataVersion(false), common.KinMintAccount) + newTokenAccount, err := newOwnerAccount.ToTimelockVault(common.KinMintAccount) require.NoError(t, err) data := &balanceTestData{ codeUsers: []*common.Account{newOwnerAccount}, } - setupBalanceTestData(t, env, data, balanceTestDataConf{}) + setupBalanceTestData(t, env, data) accountRecords, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, newOwnerAccount) require.NoError(t, err) @@ -58,290 +58,254 @@ func TestDefaultCalculationMethods_NewCodeAccount(t *testing.T) { } func TestDefaultCalculationMethods_DepositFromExternalWallet(t *testing.T) { - for _, useLegacyDeposits := range []bool{true, false} { - env := setupBalanceTestEnv(t) + env := setupBalanceTestEnv(t) - owner := testutil.NewRandomAccount(t) - depositAccount, err := owner.ToTimelockVault(getTimelockDataVersion(useLegacyDeposits), common.KinMintAccount) - require.NoError(t, err) + owner := testutil.NewRandomAccount(t) + depositAccount, err := owner.ToTimelockVault(common.KinMintAccount) + require.NoError(t, err) - externalAccount := testutil.NewRandomAccount(t) - - data := &balanceTestData{ - codeUsers: []*common.Account{owner}, - transactions: []balanceTestTransaction{ - // The following entries are added to the balance - {source: externalAccount, destination: depositAccount, quantity: 1, transactionState: transaction.ConfirmationFinalized}, - {source: externalAccount, destination: depositAccount, quantity: 10, transactionState: transaction.ConfirmationFinalized}, - // The following entries aren't added to the balance because they aren't finalized - {source: externalAccount, destination: depositAccount, quantity: 100, transactionState: transaction.ConfirmationFailed}, - {source: externalAccount, destination: depositAccount, quantity: 1000, transactionState: transaction.ConfirmationPending}, - {source: externalAccount, destination: depositAccount, quantity: 10000, transactionState: transaction.ConfirmationUnknown}, - }, - } - setupBalanceTestData(t, env, data, balanceTestDataConf{ - useLegacyIntents: useLegacyDeposits, - useLegacyDeposits: useLegacyDeposits, - }) + externalAccount := testutil.NewRandomAccount(t) - balance, err := CalculateFromCache(env.ctx, env.data, depositAccount) - require.NoError(t, err) - assert.EqualValues(t, 11, balance) + data := &balanceTestData{ + codeUsers: []*common.Account{owner}, + transactions: []balanceTestTransaction{ + // The following entries are added to the balance + {source: externalAccount, destination: depositAccount, quantity: 1, transactionState: transaction.ConfirmationFinalized}, + {source: externalAccount, destination: depositAccount, quantity: 10, transactionState: transaction.ConfirmationFinalized}, + // The following entries aren't added to the balance because they aren't finalized + {source: externalAccount, destination: depositAccount, quantity: 100, transactionState: transaction.ConfirmationFailed}, + {source: externalAccount, destination: depositAccount, quantity: 1000, transactionState: transaction.ConfirmationPending}, + {source: externalAccount, destination: depositAccount, quantity: 10000, transactionState: transaction.ConfirmationUnknown}, + }, + } + setupBalanceTestData(t, env, data) - if !useLegacyDeposits { - accountRecords, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner) - require.NoError(t, err) + balance, err := CalculateFromCache(env.ctx, env.data, depositAccount) + require.NoError(t, err) + assert.EqualValues(t, 11, balance) - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[commonpb.AccountType_PRIMARY][0]) - require.NoError(t, err) - require.Len(t, balanceByAccount, 1) - assert.EqualValues(t, 11, balanceByAccount[depositAccount.PublicKey().ToBase58()]) + accountRecords, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner) + require.NoError(t, err) - balanceByAccount, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, depositAccount) - require.NoError(t, err) - require.Len(t, balanceByAccount, 1) - assert.EqualValues(t, 11, balanceByAccount[depositAccount.PublicKey().ToBase58()]) - } - } + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[commonpb.AccountType_PRIMARY][0]) + require.NoError(t, err) + require.Len(t, balanceByAccount, 1) + assert.EqualValues(t, 11, balanceByAccount[depositAccount.PublicKey().ToBase58()]) + + balanceByAccount, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, depositAccount) + require.NoError(t, err) + require.Len(t, balanceByAccount, 1) + assert.EqualValues(t, 11, balanceByAccount[depositAccount.PublicKey().ToBase58()]) } func TestDefaultCalculationMethods_MultipleIntents(t *testing.T) { - for _, useLegacyIntents := range []bool{true, false} { - env := setupBalanceTestEnv(t) + env := setupBalanceTestEnv(t) - owner1 := testutil.NewRandomAccount(t) - a1, err := owner1.ToTimelockVault(getTimelockDataVersion(useLegacyIntents), common.KinMintAccount) - require.NoError(t, err) + owner1 := testutil.NewRandomAccount(t) + a1, err := owner1.ToTimelockVault(common.KinMintAccount) + require.NoError(t, err) - owner2 := testutil.NewRandomAccount(t) - a2, err := owner2.ToTimelockVault(getTimelockDataVersion(useLegacyIntents), common.KinMintAccount) - require.NoError(t, err) + owner2 := testutil.NewRandomAccount(t) + a2, err := owner2.ToTimelockVault(common.KinMintAccount) + require.NoError(t, err) - owner3 := testutil.NewRandomAccount(t) - a3, err := owner3.ToTimelockVault(getTimelockDataVersion(useLegacyIntents), common.KinMintAccount) - require.NoError(t, err) + owner3 := testutil.NewRandomAccount(t) + a3, err := owner3.ToTimelockVault(common.KinMintAccount) + require.NoError(t, err) - owner4 := testutil.NewRandomAccount(t) - a4, err := owner4.ToTimelockVault(getTimelockDataVersion(useLegacyIntents), common.KinMintAccount) - require.NoError(t, err) + owner4 := testutil.NewRandomAccount(t) + a4, err := owner4.ToTimelockVault(common.KinMintAccount) + require.NoError(t, err) - externalAccount := testutil.NewRandomAccount(t) - - data := &balanceTestData{ - codeUsers: []*common.Account{owner1, owner2, owner3, owner4}, - transactions: []balanceTestTransaction{ - // Fund account a1 through a4 with an external deposit - {source: externalAccount, destination: a1, quantity: 1, transactionState: transaction.ConfirmationFinalized}, - {source: externalAccount, destination: a2, quantity: 10, transactionState: transaction.ConfirmationFinalized}, - {source: externalAccount, destination: a3, quantity: 100, transactionState: transaction.ConfirmationFinalized}, - {source: externalAccount, destination: a4, quantity: 1000, transactionState: transaction.ConfirmationFinalized}, - // Confirmed intents are incorporated into balance calculations - {source: a4, destination: a1, quantity: 1, intentID: "i1", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, - {source: a4, destination: a1, quantity: 2, intentID: "i2", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, - // Pending intents are incorporated into balance calculations - {source: a4, destination: a2, quantity: 3, intentID: "i3", intentState: intent.StatePending, actionState: action.StatePending}, - {source: a4, destination: a2, quantity: 4, intentID: "i4", intentState: intent.StatePending, actionState: action.StatePending}, - // Failed intents are incorporated into balance calculations. We'll - // always make the user whole. - {source: a4, destination: a3, quantity: 5, intentID: "i5", intentState: intent.StateFailed, actionState: action.StateFailed}, - {source: a4, destination: a3, quantity: 6, intentID: "i6", intentState: intent.StateFailed, actionState: action.StateFailed}, - // Intents in the unknown state are incorporated differently depending - // on the intent type, since it infers which intent system it came from. - // Legacy intents are not incorporated, as the intent is not committed by - // the client. Intents could theoretically by in the unknown state under - // the new system, but we should limit this as much as possible. - {source: a4, destination: a1, quantity: 7, intentID: "i7", intentState: intent.StateUnknown, actionState: action.StateUnknown}, - // Revoked intents are not incorporated into balance calculations. - {source: a4, destination: a2, quantity: 8, intentID: "i8", intentState: intent.StateRevoked, actionState: action.StateRevoked}, - }, - } + externalAccount := testutil.NewRandomAccount(t) - setupBalanceTestData(t, env, data, balanceTestDataConf{ - useLegacyIntents: useLegacyIntents, - useLegacyDeposits: useLegacyIntents, - }) + data := &balanceTestData{ + codeUsers: []*common.Account{owner1, owner2, owner3, owner4}, + transactions: []balanceTestTransaction{ + // Fund account a1 through a4 with an external deposit + {source: externalAccount, destination: a1, quantity: 1, transactionState: transaction.ConfirmationFinalized}, + {source: externalAccount, destination: a2, quantity: 10, transactionState: transaction.ConfirmationFinalized}, + {source: externalAccount, destination: a3, quantity: 100, transactionState: transaction.ConfirmationFinalized}, + {source: externalAccount, destination: a4, quantity: 1000, transactionState: transaction.ConfirmationFinalized}, + // Confirmed intents are incorporated into balance calculations + {source: a4, destination: a1, quantity: 1, intentID: "i1", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, + {source: a4, destination: a1, quantity: 2, intentID: "i2", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, + // Pending intents are incorporated into balance calculations + {source: a4, destination: a2, quantity: 3, intentID: "i3", intentState: intent.StatePending, actionState: action.StatePending}, + {source: a4, destination: a2, quantity: 4, intentID: "i4", intentState: intent.StatePending, actionState: action.StatePending}, + // Failed intents are incorporated into balance calculations. We'll + // always make the user whole. + {source: a4, destination: a3, quantity: 5, intentID: "i5", intentState: intent.StateFailed, actionState: action.StateFailed}, + {source: a4, destination: a3, quantity: 6, intentID: "i6", intentState: intent.StateFailed, actionState: action.StateFailed}, + // Intents in the unknown state are incorporated differently depending + // on the intent type, since it infers which intent system it came from. + // Legacy intents are not incorporated, as the intent is not committed by + // the client. Intents could theoretically by in the unknown state under + // the new system, but we should limit this as much as possible. + {source: a4, destination: a1, quantity: 7, intentID: "i7", intentState: intent.StateUnknown, actionState: action.StateUnknown}, + // Revoked intents are not incorporated into balance calculations. + {source: a4, destination: a2, quantity: 8, intentID: "i8", intentState: intent.StateRevoked, actionState: action.StateRevoked}, + }, + } - balance, err := CalculateFromCache(env.ctx, env.data, a1) - require.NoError(t, err) - if useLegacyIntents { - assert.EqualValues(t, 4, balance) - } else { - assert.EqualValues(t, 11, balance) - } + setupBalanceTestData(t, env, data) - balance, err = CalculateFromCache(env.ctx, env.data, a2) - require.NoError(t, err) - assert.EqualValues(t, 17, balance) + balance, err := CalculateFromCache(env.ctx, env.data, a1) + require.NoError(t, err) + assert.EqualValues(t, 11, balance) - balance, err = CalculateFromCache(env.ctx, env.data, a3) - require.NoError(t, err) - assert.EqualValues(t, 111, balance) + balance, err = CalculateFromCache(env.ctx, env.data, a2) + require.NoError(t, err) + assert.EqualValues(t, 17, balance) - balance, err = CalculateFromCache(env.ctx, env.data, a4) - require.NoError(t, err) - if useLegacyIntents { - assert.EqualValues(t, 979, balance) - } else { - assert.EqualValues(t, 972, balance) - } + balance, err = CalculateFromCache(env.ctx, env.data, a3) + require.NoError(t, err) + assert.EqualValues(t, 111, balance) - if !useLegacyIntents { - accountRecords1, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner1) - require.NoError(t, err) - - accountRecords2, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner2) - require.NoError(t, err) - - accountRecords3, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner3) - require.NoError(t, err) - - accountRecords4, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner4) - require.NoError(t, err) - - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords1[commonpb.AccountType_PRIMARY][0], accountRecords2[commonpb.AccountType_PRIMARY][0], accountRecords3[commonpb.AccountType_PRIMARY][0], accountRecords4[commonpb.AccountType_PRIMARY][0]) - require.NoError(t, err) - require.Len(t, balanceByAccount, 4) - assert.EqualValues(t, 11, balanceByAccount[a1.PublicKey().ToBase58()]) - assert.EqualValues(t, 17, balanceByAccount[a2.PublicKey().ToBase58()]) - assert.EqualValues(t, 111, balanceByAccount[a3.PublicKey().ToBase58()]) - assert.EqualValues(t, 972, balanceByAccount[a4.PublicKey().ToBase58()]) - - balanceByAccount, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, a1, a2, a3, a4) - require.NoError(t, err) - require.Len(t, balanceByAccount, 4) - assert.EqualValues(t, 11, balanceByAccount[a1.PublicKey().ToBase58()]) - assert.EqualValues(t, 17, balanceByAccount[a2.PublicKey().ToBase58()]) - assert.EqualValues(t, 111, balanceByAccount[a3.PublicKey().ToBase58()]) - assert.EqualValues(t, 972, balanceByAccount[a4.PublicKey().ToBase58()]) - } - } + balance, err = CalculateFromCache(env.ctx, env.data, a4) + require.NoError(t, err) + assert.EqualValues(t, 972, balance) + + accountRecords1, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner1) + require.NoError(t, err) + + accountRecords2, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner2) + require.NoError(t, err) + + accountRecords3, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner3) + require.NoError(t, err) + + accountRecords4, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner4) + require.NoError(t, err) + + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords1[commonpb.AccountType_PRIMARY][0], accountRecords2[commonpb.AccountType_PRIMARY][0], accountRecords3[commonpb.AccountType_PRIMARY][0], accountRecords4[commonpb.AccountType_PRIMARY][0]) + require.NoError(t, err) + require.Len(t, balanceByAccount, 4) + assert.EqualValues(t, 11, balanceByAccount[a1.PublicKey().ToBase58()]) + assert.EqualValues(t, 17, balanceByAccount[a2.PublicKey().ToBase58()]) + assert.EqualValues(t, 111, balanceByAccount[a3.PublicKey().ToBase58()]) + assert.EqualValues(t, 972, balanceByAccount[a4.PublicKey().ToBase58()]) + + balanceByAccount, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, a1, a2, a3, a4) + require.NoError(t, err) + require.Len(t, balanceByAccount, 4) + assert.EqualValues(t, 11, balanceByAccount[a1.PublicKey().ToBase58()]) + assert.EqualValues(t, 17, balanceByAccount[a2.PublicKey().ToBase58()]) + assert.EqualValues(t, 111, balanceByAccount[a3.PublicKey().ToBase58()]) + assert.EqualValues(t, 972, balanceByAccount[a4.PublicKey().ToBase58()]) } func TestDefaultCalculationMethods_BackAndForth(t *testing.T) { - for _, useLegacyIntents := range []bool{true, false} { - env := setupBalanceTestEnv(t) + env := setupBalanceTestEnv(t) - owner1 := testutil.NewRandomAccount(t) - a1, err := owner1.ToTimelockVault(getTimelockDataVersion(useLegacyIntents), common.KinMintAccount) - require.NoError(t, err) + owner1 := testutil.NewRandomAccount(t) + a1, err := owner1.ToTimelockVault(common.KinMintAccount) + require.NoError(t, err) - owner2 := testutil.NewRandomAccount(t) - a2, err := owner2.ToTimelockVault(getTimelockDataVersion(useLegacyIntents), common.KinMintAccount) - require.NoError(t, err) + owner2 := testutil.NewRandomAccount(t) + a2, err := owner2.ToTimelockVault(common.KinMintAccount) + require.NoError(t, err) - externalAccount := testutil.NewRandomAccount(t) - - data := &balanceTestData{ - codeUsers: []*common.Account{owner1, owner2}, - transactions: []balanceTestTransaction{ - // Fund account a1 through an external deposit - {source: externalAccount, destination: a1, quantity: 1, transactionState: transaction.ConfirmationFinalized}, - // Setup a set of intents that result in back and forth movement of the Kin - {source: a1, destination: a2, quantity: 1, intentID: "i1", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, - {source: a2, destination: a1, quantity: 1, intentID: "i2", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, - {source: a1, destination: a2, quantity: 1, intentID: "i3", intentState: intent.StatePending, actionState: action.StatePending}, - {source: a2, destination: a1, quantity: 1, intentID: "i4", intentState: intent.StatePending, actionState: action.StatePending}, - {source: a1, destination: a2, quantity: 1, intentID: "i5", intentState: intent.StatePending, actionState: action.StatePending}, - }, - } + externalAccount := testutil.NewRandomAccount(t) + + data := &balanceTestData{ + codeUsers: []*common.Account{owner1, owner2}, + transactions: []balanceTestTransaction{ + // Fund account a1 through an external deposit + {source: externalAccount, destination: a1, quantity: 1, transactionState: transaction.ConfirmationFinalized}, + // Setup a set of intents that result in back and forth movement of the Kin + {source: a1, destination: a2, quantity: 1, intentID: "i1", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, + {source: a2, destination: a1, quantity: 1, intentID: "i2", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, + {source: a1, destination: a2, quantity: 1, intentID: "i3", intentState: intent.StatePending, actionState: action.StatePending}, + {source: a2, destination: a1, quantity: 1, intentID: "i4", intentState: intent.StatePending, actionState: action.StatePending}, + {source: a1, destination: a2, quantity: 1, intentID: "i5", intentState: intent.StatePending, actionState: action.StatePending}, + }, + } - setupBalanceTestData(t, env, data, balanceTestDataConf{ - useLegacyIntents: useLegacyIntents, - useLegacyDeposits: useLegacyIntents, - }) + setupBalanceTestData(t, env, data) - balance, err := CalculateFromCache(env.ctx, env.data, a1) - require.NoError(t, err) - assert.EqualValues(t, 0, balance) + balance, err := CalculateFromCache(env.ctx, env.data, a1) + require.NoError(t, err) + assert.EqualValues(t, 0, balance) - balance, err = CalculateFromCache(env.ctx, env.data, a2) - require.NoError(t, err) - assert.EqualValues(t, 1, balance) - - if !useLegacyIntents { - accountRecords1, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner1) - require.NoError(t, err) - - accountRecords2, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner2) - require.NoError(t, err) - - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords1[commonpb.AccountType_PRIMARY][0], accountRecords2[commonpb.AccountType_PRIMARY][0]) - require.NoError(t, err) - require.Len(t, balanceByAccount, 2) - assert.EqualValues(t, 0, balanceByAccount[a1.PublicKey().ToBase58()]) - assert.EqualValues(t, 1, balanceByAccount[a2.PublicKey().ToBase58()]) - - balanceByAccount, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, a1, a2) - require.NoError(t, err) - require.Len(t, balanceByAccount, 2) - assert.EqualValues(t, 0, balanceByAccount[a1.PublicKey().ToBase58()]) - assert.EqualValues(t, 1, balanceByAccount[a2.PublicKey().ToBase58()]) - } - } + balance, err = CalculateFromCache(env.ctx, env.data, a2) + require.NoError(t, err) + assert.EqualValues(t, 1, balance) + + accountRecords1, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner1) + require.NoError(t, err) + + accountRecords2, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner2) + require.NoError(t, err) + + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords1[commonpb.AccountType_PRIMARY][0], accountRecords2[commonpb.AccountType_PRIMARY][0]) + require.NoError(t, err) + require.Len(t, balanceByAccount, 2) + assert.EqualValues(t, 0, balanceByAccount[a1.PublicKey().ToBase58()]) + assert.EqualValues(t, 1, balanceByAccount[a2.PublicKey().ToBase58()]) + + balanceByAccount, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, a1, a2) + require.NoError(t, err) + require.Len(t, balanceByAccount, 2) + assert.EqualValues(t, 0, balanceByAccount[a1.PublicKey().ToBase58()]) + assert.EqualValues(t, 1, balanceByAccount[a2.PublicKey().ToBase58()]) } func TestDefaultCalculationMethods_SelfPayments(t *testing.T) { - for _, useLegacyIntents := range []bool{true, false} { - env := setupBalanceTestEnv(t) + env := setupBalanceTestEnv(t) - ownerAccount := testutil.NewRandomAccount(t) - tokenAccount, err := ownerAccount.ToTimelockVault(getTimelockDataVersion(useLegacyIntents), common.KinMintAccount) - require.NoError(t, err) + ownerAccount := testutil.NewRandomAccount(t) + tokenAccount, err := ownerAccount.ToTimelockVault(common.KinMintAccount) + require.NoError(t, err) - externalAccount := testutil.NewRandomAccount(t) - - data := &balanceTestData{ - codeUsers: []*common.Account{ownerAccount}, - transactions: []balanceTestTransaction{ - // Fund account the token account through an external deposit - {source: externalAccount, destination: tokenAccount, quantity: 1, transactionState: transaction.ConfirmationFinalized}, - // Setup a set of intents that result in self-payments and no-ops to - // the balance calculation - {source: tokenAccount, destination: tokenAccount, quantity: 1, intentID: "i1", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, - {source: tokenAccount, destination: tokenAccount, quantity: 1, intentID: "i2", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, - {source: tokenAccount, destination: tokenAccount, quantity: 1, intentID: "i3", intentState: intent.StatePending, actionState: action.StatePending}, - {source: tokenAccount, destination: tokenAccount, quantity: 1, intentID: "i4", intentState: intent.StatePending, actionState: action.StatePending}, - }, - } + externalAccount := testutil.NewRandomAccount(t) - setupBalanceTestData(t, env, data, balanceTestDataConf{ - useLegacyIntents: useLegacyIntents, - useLegacyDeposits: useLegacyIntents, - }) + data := &balanceTestData{ + codeUsers: []*common.Account{ownerAccount}, + transactions: []balanceTestTransaction{ + // Fund account the token account through an external deposit + {source: externalAccount, destination: tokenAccount, quantity: 1, transactionState: transaction.ConfirmationFinalized}, + // Setup a set of intents that result in self-payments and no-ops to + // the balance calculation + {source: tokenAccount, destination: tokenAccount, quantity: 1, intentID: "i1", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, + {source: tokenAccount, destination: tokenAccount, quantity: 1, intentID: "i2", intentState: intent.StateConfirmed, actionState: action.StateConfirmed, transactionState: transaction.ConfirmationFinalized}, + {source: tokenAccount, destination: tokenAccount, quantity: 1, intentID: "i3", intentState: intent.StatePending, actionState: action.StatePending}, + {source: tokenAccount, destination: tokenAccount, quantity: 1, intentID: "i4", intentState: intent.StatePending, actionState: action.StatePending}, + }, + } - balance, err := CalculateFromCache(env.ctx, env.data, tokenAccount) - require.NoError(t, err) - assert.EqualValues(t, 1, balance) + setupBalanceTestData(t, env, data) - if !useLegacyIntents { - accountRecords, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, ownerAccount) - require.NoError(t, err) + balance, err := CalculateFromCache(env.ctx, env.data, tokenAccount) + require.NoError(t, err) + assert.EqualValues(t, 1, balance) - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[commonpb.AccountType_PRIMARY][0]) - require.NoError(t, err) - require.Len(t, balanceByAccount, 1) - assert.EqualValues(t, 1, balanceByAccount[tokenAccount.PublicKey().ToBase58()]) + accountRecords, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, ownerAccount) + require.NoError(t, err) - balanceByAccount, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, tokenAccount) - require.NoError(t, err) - require.Len(t, balanceByAccount, 1) - assert.EqualValues(t, 1, balanceByAccount[tokenAccount.PublicKey().ToBase58()]) - } - } + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[commonpb.AccountType_PRIMARY][0]) + require.NoError(t, err) + require.Len(t, balanceByAccount, 1) + assert.EqualValues(t, 1, balanceByAccount[tokenAccount.PublicKey().ToBase58()]) + + balanceByAccount, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, tokenAccount) + require.NoError(t, err) + require.Len(t, balanceByAccount, 1) + assert.EqualValues(t, 1, balanceByAccount[tokenAccount.PublicKey().ToBase58()]) } func TestDefaultCalculationMethods_NotManagedByCode(t *testing.T) { env := setupBalanceTestEnv(t) ownerAccount := testutil.NewRandomAccount(t) - tokenAccount, err := ownerAccount.ToTimelockVault(getTimelockDataVersion(false), common.KinMintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(common.KinMintAccount) require.NoError(t, err) data := &balanceTestData{ codeUsers: []*common.Account{ownerAccount}, } - setupBalanceTestData(t, env, data, balanceTestDataConf{}) + setupBalanceTestData(t, env, data) timelockRecord, err := env.data.GetTimelockByVault(env.ctx, tokenAccount.PublicKey().ToBase58()) require.NoError(t, err) @@ -362,31 +326,6 @@ func TestDefaultCalculationMethods_NotManagedByCode(t *testing.T) { assert.Equal(t, ErrNotManagedByCode, err) } -func TestDefaultBatchCalculation_PrePrivacyAccounts(t *testing.T) { - env := setupBalanceTestEnv(t) - - ownerAccount := testutil.NewRandomAccount(t) - legacyTokenAccount, err := ownerAccount.ToTimelockVault(getTimelockDataVersion(true), common.KinMintAccount) - require.NoError(t, err) - - data := &balanceTestData{ - codeUsers: []*common.Account{ownerAccount}, - } - - setupBalanceTestData(t, env, data, balanceTestDataConf{ - useLegacyIntents: true, - }) - - timelockRecord, err := env.data.GetTimelockByVault(env.ctx, legacyTokenAccount.PublicKey().ToBase58()) - require.NoError(t, err) - - _, err = BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, &common.AccountRecords{Timelock: timelockRecord}) - assert.Equal(t, ErrUnhandledAccount, err) - - _, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, legacyTokenAccount) - assert.Equal(t, ErrUnhandledAccount, err) -} - func TestDefaultCalculation_ExternalAccount(t *testing.T) { env := setupBalanceTestEnv(t) externalAccount := testutil.NewRandomAccount(t) @@ -421,7 +360,7 @@ func TestGetAggregatedBalances(t *testing.T) { expectedPrivateBalance += balance } - timelockAccounts, err := authority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(common.KinMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -431,7 +370,7 @@ func TestGetAggregatedBalances(t *testing.T) { OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: authority.PublicKey().ToBase58(), TokenAccount: timelockRecord.VaultAddress, - MintAccount: timelockRecord.Mint, + MintAccount: common.KinMintAccount.PublicKey().ToBase58(), AccountType: accountType, } if accountType == commonpb.AccountType_RELATIONSHIP { @@ -483,88 +422,61 @@ func setupBalanceTestEnv(t *testing.T) (env balanceTestEnv) { return env } -type balanceTestDataConf struct { - useLegacyIntents bool - useLegacyDeposits bool -} - -func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestData, conf balanceTestDataConf) { +func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestData) { for _, owner := range data.codeUsers { - timelockAccounts, err := owner.GetTimelockAccounts(getTimelockDataVersion(conf.useLegacyIntents), common.KinMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(common.KinMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() timelockRecord.VaultState = timelock_token_v1.StateLocked timelockRecord.Block += 1 require.NoError(t, env.data.SaveTimelock(env.ctx, timelockRecord)) - if !conf.useLegacyIntents { - accountInfoRecord := &account.Record{ - OwnerAccount: owner.PublicKey().ToBase58(), - AuthorityAccount: owner.PublicKey().ToBase58(), - TokenAccount: timelockRecord.VaultAddress, - MintAccount: timelockRecord.Mint, - AccountType: commonpb.AccountType_PRIMARY, - } - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) + accountInfoRecord := &account.Record{ + OwnerAccount: owner.PublicKey().ToBase58(), + AuthorityAccount: owner.PublicKey().ToBase58(), + TokenAccount: timelockRecord.VaultAddress, + MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + AccountType: commonpb.AccountType_PRIMARY, } + require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) } for i, txn := range data.transactions { // Setup the intent record with an equivalent action record if len(txn.intentID) > 0 { - if conf.useLegacyIntents { - intentRecord := &intent.Record{ - IntentId: txn.intentID, - IntentType: intent.LegacyPayment, - InitiatorOwnerAccount: "owner", - MoneyTransferMetadata: &intent.MoneyTransferMetadata{ - Source: txn.source.PublicKey().ToBase58(), - Destination: txn.destination.PublicKey().ToBase58(), - Quantity: txn.quantity, - - ExchangeCurrency: currency.KIN, - ExchangeRate: 1.0, - UsdMarketValue: 1.0, - }, - State: txn.intentState, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) - } else { - intentRecord := &intent.Record{ - IntentId: txn.intentID, - IntentType: intent.SendPrivatePayment, - InitiatorOwnerAccount: "owner", - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - DestinationOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - DestinationTokenAccount: txn.destination.PublicKey().ToBase58(), - Quantity: txn.quantity, - - ExchangeCurrency: currency.KIN, - ExchangeRate: 1.0, - NativeAmount: 1.0, - UsdMarketValue: 1.0, - }, - State: txn.intentState, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) - - actionRecord := &action.Record{ - Intent: txn.intentID, - IntentType: intent.SendPrivatePayment, - - ActionId: 0, - ActionType: action.PrivateTransfer, - - Source: txn.source.PublicKey().ToBase58(), - Destination: &intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount, - Quantity: &intentRecord.SendPrivatePaymentMetadata.Quantity, - - State: txn.actionState, - } - require.NoError(t, env.data.PutAllActions(env.ctx, actionRecord)) + intentRecord := &intent.Record{ + IntentId: txn.intentID, + IntentType: intent.SendPrivatePayment, + InitiatorOwnerAccount: "owner", + SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ + DestinationOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + DestinationTokenAccount: txn.destination.PublicKey().ToBase58(), + Quantity: txn.quantity, + + ExchangeCurrency: currency.KIN, + ExchangeRate: 1.0, + NativeAmount: 1.0, + UsdMarketValue: 1.0, + }, + State: txn.intentState, + CreatedAt: time.Now(), + } + require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) + + actionRecord := &action.Record{ + Intent: txn.intentID, + IntentType: intent.SendPrivatePayment, + + ActionId: 0, + ActionType: action.PrivateTransfer, + + Source: txn.source.PublicKey().ToBase58(), + Destination: &intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount, + Quantity: &intentRecord.SendPrivatePaymentMetadata.Quantity, + + State: txn.actionState, } + require.NoError(t, env.data.PutAllActions(env.ctx, actionRecord)) } // We have an intent, and it's confirmed, so a payment record exists @@ -593,57 +505,20 @@ func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestDat require.NoError(t, env.data.CreatePayment(env.ctx, paymentRecord)) } - // There's no intent, so we have an external deposit. Depending on legacy - // status, we either have a legacy payment record or a new deposit record. - // - // todo: configuration for legacy deposits - if len(txn.intentID) == 0 { - if conf.useLegacyDeposits { - paymentRecord := &payment.Record{ - Source: txn.source.PublicKey().ToBase58(), - Destination: txn.destination.PublicKey().ToBase58(), - Quantity: txn.quantity, - - Rendezvous: "", - IsExternal: true, - - TransactionId: fmt.Sprintf("txn%d", i), - - ConfirmationState: txn.transactionState, - - // Below fields are irrelevant and can be set to whatever - ExchangeCurrency: string(currency.KIN), - ExchangeRate: 1.0, - UsdMarketValue: 1.0, - - BlockId: 12345, - - CreatedAt: time.Now(), - } - require.NoError(t, env.data.CreatePayment(env.ctx, paymentRecord)) - } + // There's no intent, so we have an external deposit + if len(txn.intentID) == 0 && txn.transactionState != transaction.ConfirmationUnknown { + depositRecord := &deposit.Record{ + Signature: fmt.Sprintf("txn%d", i), + Destination: txn.destination.PublicKey().ToBase58(), + Amount: txn.quantity, + UsdMarketValue: 1.0, - if !conf.useLegacyDeposits && txn.transactionState != transaction.ConfirmationUnknown { - depositRecord := &deposit.Record{ - Signature: fmt.Sprintf("txn%d", i), - Destination: txn.destination.PublicKey().ToBase58(), - Amount: txn.quantity, - UsdMarketValue: 1.0, - - Slot: 12345, - ConfirmationState: txn.transactionState, + Slot: 12345, + ConfirmationState: txn.transactionState, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.SaveExternalDeposit(env.ctx, depositRecord)) + CreatedAt: time.Now(), } + require.NoError(t, env.data.SaveExternalDeposit(env.ctx, depositRecord)) } } } - -func getTimelockDataVersion(useLegacyIntents bool) timelock_token_v1.TimelockDataVersion { - if useLegacyIntents { - return timelock_token_v1.DataVersionLegacy - } - return timelock_token_v1.DataVersion1 -} diff --git a/pkg/code/common/account.go b/pkg/code/common/account.go index d48d971d..46903ee1 100644 --- a/pkg/code/common/account.go +++ b/pkg/code/common/account.go @@ -7,53 +7,27 @@ import ( "fmt" "github.com/pkg/errors" - "github.com/sirupsen/logrus" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/timelock" - "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/solana" - timelock_token_legacy "github.com/code-payments/code-server/pkg/solana/timelock/legacy_2022" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/solana/token" ) -const ( - dangerousTimelockAccessCountMetricName = "Account/dangerous_timelock_access_count" -) - -var ( - // defaultTimelockNonceAccount is the default nonce account used to derive - // legacy 2022 timelock PDAs. - // - // Important Note: Be very careful changing this value, as it will completely - // change timelock PDAs. - defaultTimelockNonceAccount *Account -) - var ( ErrNoPrivacyMigration2022 = errors.New("no privacy migration 2022 for owner") ) -func init() { - var err error - defaultTimelockNonceAccount, err = NewAccountFromPublicKeyBytes(make([]byte, ed25519.PublicKeySize)) - if err != nil { - panic(err) - } -} - type Account struct { publicKey *Key privateKey *Key // Optional } type TimelockAccounts struct { - DataVersion timelock_token_v1.TimelockDataVersion - State *Account StateBump uint8 @@ -62,9 +36,6 @@ type TimelockAccounts struct { VaultOwner *Account - TimeAuthority *Account - CloseAuthority *Account - Mint *Account } @@ -184,12 +155,12 @@ func (a *Account) Sign(message []byte) ([]byte, error) { return signature, nil } -func (a *Account) ToTimelockVault(dataVersion timelock_token_v1.TimelockDataVersion, mint *Account) (*Account, error) { +func (a *Account) ToTimelockVault(mint *Account) (*Account, error) { if err := a.Validate(); err != nil { return nil, errors.Wrap(err, "error validating owner account") } - timelockAccounts, err := a.GetTimelockAccounts(dataVersion, mint) + timelockAccounts, err := a.GetTimelockAccounts(mint) if err != nil { return nil, err } @@ -209,98 +180,50 @@ func (a *Account) ToAssociatedTokenAccount(mint *Account) (*Account, error) { return NewAccountFromPublicKeyBytes(ata) } -func (a *Account) GetTimelockAccounts(dataVersion timelock_token_v1.TimelockDataVersion, mint *Account) (*TimelockAccounts, error) { +func (a *Account) GetTimelockAccounts(mint *Account) (*TimelockAccounts, error) { if err := a.Validate(); err != nil { return nil, errors.Wrap(err, "error validating owner account") } - var timelockAccounts *TimelockAccounts - switch dataVersion { - case timelock_token_v1.DataVersion1: - stateAddress, stateBump, err := timelock_token_v1.GetStateAddress(&timelock_token_v1.GetStateAddressArgs{ - Mint: mint.publicKey.ToBytes(), - TimeAuthority: GetSubsidizer().publicKey.ToBytes(), - VaultOwner: a.publicKey.ToBytes(), - NumDaysLocked: timelock_token_v1.DefaultNumDaysLocked, - }) - if err != nil { - return nil, errors.Wrap(err, "error getting timelock state address") - } - - vaultAddress, vaultBump, err := timelock_token_v1.GetVaultAddress(&timelock_token_v1.GetVaultAddressArgs{ - State: stateAddress, - DataVersion: timelock_token_v1.DataVersion1, - }) - if err != nil { - return nil, errors.Wrap(err, "error getting vault address") - } - - stateAccount, err := NewAccountFromPublicKeyBytes(stateAddress) - if err != nil { - return nil, errors.Wrap(err, "invalid state address") - } - - vaultAccount, err := NewAccountFromPublicKeyBytes(vaultAddress) - if err != nil { - return nil, errors.Wrap(err, "invalid vault address") - } - - timelockAccounts = &TimelockAccounts{ - State: stateAccount, - StateBump: stateBump, - - Vault: vaultAccount, - VaultBump: vaultBump, - - Mint: mint, - } - case timelock_token_v1.DataVersionLegacy: - stateAddress, stateBump, err := timelock_token_legacy.GetStateAddress(&timelock_token_legacy.GetStateAddressArgs{ - Mint: mint.publicKey.ToBytes(), - TimeAuthority: GetSubsidizer().publicKey.ToBytes(), - Nonce: defaultTimelockNonceAccount.publicKey.ToBytes(), - VaultOwner: a.publicKey.ToBytes(), - UnlockDuration: timelock_token_legacy.DefaultUnlockDuration, - }) - if err != nil { - return nil, errors.Wrap(err, "error getting timelock state address") - } + stateAddress, stateBump, err := timelock_token_v1.GetStateAddress(&timelock_token_v1.GetStateAddressArgs{ + Mint: mint.publicKey.ToBytes(), + TimeAuthority: GetSubsidizer().publicKey.ToBytes(), + VaultOwner: a.publicKey.ToBytes(), + NumDaysLocked: timelock_token_v1.DefaultNumDaysLocked, + }) + if err != nil { + return nil, errors.Wrap(err, "error getting timelock state address") + } - vaultAddress, vaultBump, err := timelock_token_legacy.GetVaultAddress(&timelock_token_legacy.GetVaultAddressArgs{ - State: stateAddress, - }) - if err != nil { - return nil, errors.Wrap(err, "error getting vault address") - } + vaultAddress, vaultBump, err := timelock_token_v1.GetVaultAddress(&timelock_token_v1.GetVaultAddressArgs{ + State: stateAddress, + DataVersion: timelock_token_v1.DataVersion1, + }) + if err != nil { + return nil, errors.Wrap(err, "error getting vault address") + } - stateAccount, err := NewAccountFromPublicKeyBytes(stateAddress) - if err != nil { - return nil, errors.Wrap(err, "invalid state address") - } + stateAccount, err := NewAccountFromPublicKeyBytes(stateAddress) + if err != nil { + return nil, errors.Wrap(err, "invalid state address") + } - vaultAccount, err := NewAccountFromPublicKeyBytes(vaultAddress) - if err != nil { - return nil, errors.Wrap(err, "invalid vault address") - } + vaultAccount, err := NewAccountFromPublicKeyBytes(vaultAddress) + if err != nil { + return nil, errors.Wrap(err, "invalid vault address") + } - timelockAccounts = &TimelockAccounts{ - State: stateAccount, - StateBump: stateBump, + return &TimelockAccounts{ + VaultOwner: a, - Vault: vaultAccount, - VaultBump: vaultBump, + State: stateAccount, + StateBump: stateBump, - Mint: mint, - } - default: - return nil, errors.New("unsupported data version") - } + Vault: vaultAccount, + VaultBump: vaultBump, - timelockAccounts.DataVersion = dataVersion - timelockAccounts.VaultOwner = a - timelockAccounts.TimeAuthority = GetSubsidizer() - timelockAccounts.CloseAuthority = GetSubsidizer() - return timelockAccounts, nil + Mint: mint, + }, nil } func (a *Account) IsManagedByCode(ctx context.Context, data code_data.Provider) (bool, error) { @@ -364,32 +287,7 @@ func (r *AccountRecords) IsTimelock() bool { } func IsManagedByCode(ctx context.Context, timelockRecord *timelock.Record) bool { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "type": "common/account", - "method": "IsManagedByCode", - "vault": timelockRecord.VaultAddress, - }) - - // This should never happen, but is a precautionary check. - if timelockRecord.DataVersion == timelock_token_v1.DataVersionClosed { - metrics.RecordCount(ctx, dangerousTimelockAccessCountMetricName, 1) - log.Warn("detected a dangerous timelock account with a closed data version") - return false - } - - // This should never happen, but is a precautionary check. - if timelockRecord.TimeAuthority != GetSubsidizer().publicKey.ToBase58() { - metrics.RecordCount(ctx, dangerousTimelockAccessCountMetricName, 1) - log.Warn("detected a dangerous timelock account with a time authority that's not Code") - return false - } - - // This should never happen, but is a precautionary check. - if timelockRecord.CloseAuthority != GetSubsidizer().publicKey.ToBase58() { - metrics.RecordCount(ctx, dangerousTimelockAccessCountMetricName, 1) - log.Warn("detected a dangerous timelock account with a close authority that's not Code") - return false - } + // todo: check if the VM is managed by Code // todo: We don't support unlocking timelock accounts and leaving the open, // but we may need to scan the intents system for a RevokeWithAuthority @@ -400,8 +298,6 @@ func IsManagedByCode(ctx context.Context, timelockRecord *timelock.Record) bool // ToDBRecord transforms the TimelockAccounts struct to a default timelock.Record func (a *TimelockAccounts) ToDBRecord() *timelock.Record { return &timelock.Record{ - DataVersion: a.DataVersion, - Address: a.State.publicKey.ToBase58(), Bump: a.StateBump, @@ -410,13 +306,7 @@ func (a *TimelockAccounts) ToDBRecord() *timelock.Record { VaultOwner: a.VaultOwner.publicKey.ToBase58(), VaultState: timelock_token_v1.StateUnknown, - TimeAuthority: a.TimeAuthority.publicKey.ToBase58(), - CloseAuthority: a.CloseAuthority.publicKey.ToBase58(), - - Mint: a.Mint.publicKey.ToBase58(), - - NumDaysLocked: timelock_token_v1.DefaultNumDaysLocked, - UnlockAt: nil, + UnlockAt: nil, Block: 0, } @@ -428,28 +318,6 @@ func (a *TimelockAccounts) GetDBRecord(ctx context.Context, data code_data.Provi return data.GetTimelockByVault(ctx, a.Vault.publicKey.ToBase58()) } -// GetInitializeInstruction gets an Initialize instruction for a timelock account -func (a *TimelockAccounts) GetInitializeInstruction() (solana.Instruction, error) { - switch a.DataVersion { - case timelock_token_v1.DataVersion1: - return timelock_token_v1.NewInitializeInstruction( - &timelock_token_v1.InitializeInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - Mint: a.Mint.publicKey.ToBytes(), - TimeAuthority: a.TimeAuthority.publicKey.ToBytes(), - Payer: a.CloseAuthority.publicKey.ToBytes(), - }, - &timelock_token_v1.InitializeInstructionArgs{ - NumDaysLocked: timelock_token_v1.DefaultNumDaysLocked, - }, - ).ToLegacyInstruction(), nil - default: - return solana.Instruction{}, errors.New("unsupported data version") - } -} - // GetTransferWithAuthorityInstruction gets a TransferWithAuthority instruction for a timelock account func (a *TimelockAccounts) GetTransferWithAuthorityInstruction(destination *Account, quarks uint64) (solana.Instruction, error) { if err := destination.Validate(); err != nil { @@ -460,25 +328,20 @@ func (a *TimelockAccounts) GetTransferWithAuthorityInstruction(destination *Acco return solana.Instruction{}, errors.New("quarks must be positive") } - switch a.DataVersion { - case timelock_token_v1.DataVersion1: - return timelock_token_v1.NewTransferWithAuthorityInstruction( - &timelock_token_v1.TransferWithAuthorityInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - TimeAuthority: a.TimeAuthority.publicKey.ToBytes(), - Destination: destination.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.TransferWithAuthorityInstructionArgs{ - TimelockBump: a.StateBump, - Amount: quarks, - }, - ).ToLegacyInstruction(), nil - default: - return solana.Instruction{}, errors.New("unsupported data version") - } + return timelock_token_v1.NewTransferWithAuthorityInstruction( + &timelock_token_v1.TransferWithAuthorityInstructionAccounts{ + Timelock: a.State.publicKey.ToBytes(), + Vault: a.Vault.publicKey.ToBytes(), + VaultOwner: a.VaultOwner.publicKey.ToBytes(), + TimeAuthority: GetSubsidizer().publicKey.ToBytes(), + Destination: destination.publicKey.ToBytes(), + Payer: GetSubsidizer().publicKey.ToBytes(), + }, + &timelock_token_v1.TransferWithAuthorityInstructionArgs{ + TimelockBump: a.StateBump, + Amount: quarks, + }, + ).ToLegacyInstruction(), nil } // GetWithdrawInstruction gets a Withdraw instruction for a timelock account @@ -487,36 +350,18 @@ func (a *TimelockAccounts) GetWithdrawInstruction(destination *Account) (solana. return solana.Instruction{}, err } - switch a.DataVersion { - case timelock_token_v1.DataVersion1: - return timelock_token_v1.NewWithdrawInstruction( - &timelock_token_v1.WithdrawInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - Destination: destination.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.WithdrawInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil - case timelock_token_v1.DataVersionLegacy: - return timelock_token_legacy.NewWithdrawInstruction( - &timelock_token_legacy.WithdrawInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - Destination: destination.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_legacy.WithdrawInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil - default: - return solana.Instruction{}, errors.New("unsupported data version") - } + return timelock_token_v1.NewWithdrawInstruction( + &timelock_token_v1.WithdrawInstructionAccounts{ + Timelock: a.State.publicKey.ToBytes(), + Vault: a.Vault.publicKey.ToBytes(), + VaultOwner: a.VaultOwner.publicKey.ToBytes(), + Destination: destination.publicKey.ToBytes(), + Payer: GetSubsidizer().publicKey.ToBytes(), + }, + &timelock_token_v1.WithdrawInstructionArgs{ + TimelockBump: a.StateBump, + }, + ).ToLegacyInstruction(), nil } // GetBurnDustWithAuthorityInstruction gets a BurnDustWithAuthority instruction for a timelock account @@ -525,134 +370,64 @@ func (a *TimelockAccounts) GetBurnDustWithAuthorityInstruction(maxQuarks uint64) return solana.Instruction{}, errors.New("max quarks must be positive") } - switch a.DataVersion { - case timelock_token_v1.DataVersion1: - return timelock_token_v1.NewBurnDustWithAuthorityInstruction( - &timelock_token_v1.BurnDustWithAuthorityInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - TimeAuthority: a.TimeAuthority.publicKey.ToBytes(), - Mint: a.Mint.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.BurnDustWithAuthorityInstructionArgs{ - TimelockBump: a.StateBump, - MaxAmount: maxQuarks, - }, - ).ToLegacyInstruction(), nil - case timelock_token_v1.DataVersionLegacy: - return timelock_token_legacy.NewBurnDustWithAuthorityInstruction( - &timelock_token_legacy.BurnDustWithAuthorityInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - TimeAuthority: a.TimeAuthority.publicKey.ToBytes(), - Mint: a.Mint.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_legacy.BurnDustWithAuthorityInstructionArgs{ - TimelockBump: a.StateBump, - MaxAmount: maxQuarks, - }, - ).ToLegacyInstruction(), nil - default: - return solana.Instruction{}, errors.New("unsupported data version") - } + return timelock_token_v1.NewBurnDustWithAuthorityInstruction( + &timelock_token_v1.BurnDustWithAuthorityInstructionAccounts{ + Timelock: a.State.publicKey.ToBytes(), + Vault: a.Vault.publicKey.ToBytes(), + VaultOwner: a.VaultOwner.publicKey.ToBytes(), + TimeAuthority: GetSubsidizer().publicKey.ToBytes(), + Mint: a.Mint.publicKey.ToBytes(), + Payer: GetSubsidizer().publicKey.ToBytes(), + }, + &timelock_token_v1.BurnDustWithAuthorityInstructionArgs{ + TimelockBump: a.StateBump, + MaxAmount: maxQuarks, + }, + ).ToLegacyInstruction(), nil } // GetRevokeLockWithAuthorityInstruction gets a RevokeLockWithAuthority instruction for a timelock account func (a *TimelockAccounts) GetRevokeLockWithAuthorityInstruction() (solana.Instruction, error) { - switch a.DataVersion { - case timelock_token_v1.DataVersion1: - return timelock_token_v1.NewRevokeLockWithAuthorityInstruction( - &timelock_token_v1.RevokeLockWithAuthorityInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - TimeAuthority: a.TimeAuthority.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.RevokeLockWithAuthorityInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil - case timelock_token_v1.DataVersionLegacy: - return timelock_token_legacy.NewRevokeLockWithAuthorityInstruction( - &timelock_token_legacy.RevokeLockWithAuthorityInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - TimeAuthority: a.TimeAuthority.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_legacy.RevokeLockWithAuthorityInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil - default: - return solana.Instruction{}, errors.New("unsupported data version") - } + return timelock_token_v1.NewRevokeLockWithAuthorityInstruction( + &timelock_token_v1.RevokeLockWithAuthorityInstructionAccounts{ + Timelock: a.State.publicKey.ToBytes(), + Vault: a.Vault.publicKey.ToBytes(), + TimeAuthority: GetSubsidizer().publicKey.ToBytes(), + Payer: GetSubsidizer().publicKey.ToBytes(), + }, + &timelock_token_v1.RevokeLockWithAuthorityInstructionArgs{ + TimelockBump: a.StateBump, + }, + ).ToLegacyInstruction(), nil } // GetDeactivateInstruction gets a Deactivate instruction for a timelock account func (a *TimelockAccounts) GetDeactivateInstruction() (solana.Instruction, error) { - switch a.DataVersion { - case timelock_token_v1.DataVersion1: - return timelock_token_v1.NewDeactivateInstruction( - &timelock_token_v1.DeactivateInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.DeactivateInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil - case timelock_token_v1.DataVersionLegacy: - return timelock_token_legacy.NewDeactivateInstruction( - &timelock_token_legacy.DeactivateInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_legacy.DeactivateInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil - default: - return solana.Instruction{}, errors.New("unsupported data version") - } + return timelock_token_v1.NewDeactivateInstruction( + &timelock_token_v1.DeactivateInstructionAccounts{ + Timelock: a.State.publicKey.ToBytes(), + VaultOwner: a.VaultOwner.publicKey.ToBytes(), + Payer: GetSubsidizer().publicKey.ToBytes(), + }, + &timelock_token_v1.DeactivateInstructionArgs{ + TimelockBump: a.StateBump, + }, + ).ToLegacyInstruction(), nil } // GetCloseAccountsInstruction gets a CloseAccounts instruction for a timelock account func (a *TimelockAccounts) GetCloseAccountsInstruction() (solana.Instruction, error) { - switch a.DataVersion { - case timelock_token_v1.DataVersion1: - return timelock_token_v1.NewCloseAccountsInstruction( - &timelock_token_v1.CloseAccountsInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - CloseAuthority: a.CloseAuthority.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.CloseAccountsInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil - case timelock_token_v1.DataVersionLegacy: - return timelock_token_legacy.NewCloseAccountsInstruction( - &timelock_token_legacy.CloseAccountsInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - CloseAuthority: a.CloseAuthority.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_legacy.CloseAccountsInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil - default: - return solana.Instruction{}, errors.New("unsupported data version") - } + return timelock_token_v1.NewCloseAccountsInstruction( + &timelock_token_v1.CloseAccountsInstructionAccounts{ + Timelock: a.State.publicKey.ToBytes(), + Vault: a.Vault.publicKey.ToBytes(), + CloseAuthority: GetSubsidizer().publicKey.ToBytes(), + Payer: GetSubsidizer().publicKey.ToBytes(), + }, + &timelock_token_v1.CloseAccountsInstructionArgs{ + TimelockBump: a.StateBump, + }, + ).ToLegacyInstruction(), nil } // ValidateExternalKinTokenAccount validates an address is an external Kin token account diff --git a/pkg/code/common/account_test.go b/pkg/code/common/account_test.go index f0f1ccd5..b66a7ac0 100644 --- a/pkg/code/common/account_test.go +++ b/pkg/code/common/account_test.go @@ -14,7 +14,6 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/solana" - timelock_token_legacy "github.com/code-payments/code-server/pkg/solana/timelock/legacy_2022" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/solana/token" ) @@ -104,7 +103,7 @@ func TestInvalidAccount(t *testing.T) { assert.Error(t, err) } -func TestConvertToTimelockVault_V1Program(t *testing.T) { +func TestConvertToTimelockVault(t *testing.T) { subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) @@ -123,12 +122,12 @@ func TestConvertToTimelockVault_V1Program(t *testing.T) { }) require.NoError(t, err) - tokenAccount, err := ownerAccount.ToTimelockVault(timelock_token_v1.DataVersion1, mintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(mintAccount) require.NoError(t, err) assert.EqualValues(t, expectedVaultAddress, tokenAccount.PublicKey().ToBytes()) } -func TestGetTimelockAccounts_V1Program(t *testing.T) { +func TestGetTimelockAccounts(t *testing.T) { subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) @@ -147,27 +146,24 @@ func TestGetTimelockAccounts_V1Program(t *testing.T) { }) require.NoError(t, err) - actual, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + actual, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) - assert.Equal(t, timelock_token_v1.DataVersion1, actual.DataVersion) assert.EqualValues(t, expectedStateAddress, actual.State.PublicKey().ToBytes()) assert.Equal(t, expectedStateBump, actual.StateBump) assert.EqualValues(t, expectedVaultAddress, actual.Vault.PublicKey().ToBytes()) assert.Equal(t, expectedVaultBump, actual.VaultBump) assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), actual.VaultOwner.PublicKey().ToBytes()) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), actual.TimeAuthority.PublicKey().ToBytes()) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), actual.CloseAuthority.PublicKey().ToBytes()) assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), actual.Mint.PublicKey().ToBytes()) } -func TestIsAccountManagedByCode_TimelockState_V1Program(t *testing.T) { +func TestIsAccountManagedByCode_TimelockState(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) // No record of the account anywhere @@ -210,14 +206,14 @@ func TestIsAccountManagedByCode_TimelockState_V1Program(t *testing.T) { assert.False(t, result) } -func TestIsAccountManagedByCode_OtherAccounts_V1Program(t *testing.T) { +func TestIsAccountManagedByCode_OtherAccounts(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) require.NoError(t, data.SaveTimelock(ctx, timelockAccounts.ToDBRecord())) @@ -234,110 +230,12 @@ func TestIsAccountManagedByCode_OtherAccounts_V1Program(t *testing.T) { assert.False(t, result) } -func TestIsAccountManagedByCode_TimeAuthority_V1Program(t *testing.T) { - ctx := context.Background() - data := code_data.NewTestDataProvider() - - subsidizerAccount = newRandomTestAccount(t) - - timeAuthorities := []*Account{ - subsidizerAccount, - newRandomTestAccount(t), - } - - mintAccount := newRandomTestAccount(t) - - for _, timeAuthority := range timeAuthorities { - ownerAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) - require.NoError(t, err) - timelockRecord := timelockAccounts.ToDBRecord() - timelockRecord.TimeAuthority = timeAuthority.PublicKey().ToBase58() - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err := timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.Equal(t, timeAuthority.PublicKey().ToBase58() == subsidizerAccount.PublicKey().ToBase58(), result) - } -} - -func TestIsAccountManagedByCode_CloseAuthority_V1Program(t *testing.T) { - ctx := context.Background() - data := code_data.NewTestDataProvider() - - subsidizerAccount = newRandomTestAccount(t) - - closeAuthorities := []*Account{ - subsidizerAccount, - newRandomTestAccount(t), - } - - mintAccount := newRandomTestAccount(t) - - for _, closeAuthority := range closeAuthorities { - ownerAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) - require.NoError(t, err) - timelockRecord := timelockAccounts.ToDBRecord() - timelockRecord.CloseAuthority = closeAuthority.PublicKey().ToBase58() - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err := timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.Equal(t, closeAuthority.PublicKey().ToBase58() == subsidizerAccount.PublicKey().ToBase58(), result) - } -} - -func TestIsAccountManagedByCode_DataVersionClosed_V1Program(t *testing.T) { - ctx := context.Background() - data := code_data.NewTestDataProvider() - - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) - require.NoError(t, err) - timelockRecord := timelockAccounts.ToDBRecord() - timelockRecord.DataVersion = timelock_token_v1.DataVersionClosed - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err := timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.False(t, result) -} - -func TestGetInitializeInstruction_V1Program(t *testing.T) { +func TestGetTransferWithAuthorityInstruction(t *testing.T) { subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) - require.NoError(t, err) - - ixn, err := timelockAccounts.GetInitializeInstruction() - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.InitializeInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelock_token_v1.DefaultNumDaysLocked, args.NumDaysLocked) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, timelockAccounts.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), accounts.VaultOwner) - assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), accounts.Mint) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.TimeAuthority) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetTransferWithAuthorityInstruction_V1Program(t *testing.T) { - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - source, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + source, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) destination := newRandomTestAccount(t) @@ -362,12 +260,12 @@ func TestGetTransferWithAuthorityInstruction_V1Program(t *testing.T) { assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) } -func TestGetWithdrawInstruction_V1Program(t *testing.T) { +func TestGetWithdrawInstruction(t *testing.T) { subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - source, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + source, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) destination := newRandomTestAccount(t) @@ -389,12 +287,12 @@ func TestGetWithdrawInstruction_V1Program(t *testing.T) { assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) } -func TestGetBurnDustWithAuthorityInstruction_V1Program(t *testing.T) { +func TestGetBurnDustWithAuthorityInstruction(t *testing.T) { subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) maxAmount := kin.ToQuarks(1) @@ -418,12 +316,12 @@ func TestGetBurnDustWithAuthorityInstruction_V1Program(t *testing.T) { assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) } -func TestGetRevokeLockWithAuthorityInstruction_V1Program(t *testing.T) { +func TestGetRevokeLockWithAuthorityInstruction(t *testing.T) { subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) ixn, err := timelockAccounts.GetRevokeLockWithAuthorityInstruction() @@ -442,363 +340,12 @@ func TestGetRevokeLockWithAuthorityInstruction_V1Program(t *testing.T) { assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) } -func TestGetDeactivateInstruction_V1Program(t *testing.T) { - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) - require.NoError(t, err) - - ixn, err := timelockAccounts.GetDeactivateInstruction() - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.DeactivateInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelockAccounts.StateBump, args.TimelockBump) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), accounts.VaultOwner) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetCloseAccountsInstruction_V1Program(t *testing.T) { - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) - require.NoError(t, err) - - ixn, err := timelockAccounts.GetCloseAccountsInstruction() - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.CloseAccountsInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelockAccounts.StateBump, args.TimelockBump) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, timelockAccounts.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.CloseAuthority) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestConvertToTimelockVault_Legacy2022Program(t *testing.T) { - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - stateAddress, _, err := timelock_token_legacy.GetStateAddress(&timelock_token_legacy.GetStateAddressArgs{ - Mint: mintAccount.PublicKey().ToBytes(), - TimeAuthority: subsidizerAccount.PublicKey().ToBytes(), - Nonce: defaultTimelockNonceAccount.PublicKey().ToBytes(), - VaultOwner: ownerAccount.PublicKey().ToBytes(), - UnlockDuration: timelock_token_legacy.DefaultUnlockDuration, - }) - require.NoError(t, err) - - expectedVaultAddress, _, err := timelock_token_legacy.GetVaultAddress(&timelock_token_legacy.GetVaultAddressArgs{ - State: stateAddress, - }) - require.NoError(t, err) - - tokenAccount, err := ownerAccount.ToTimelockVault(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - assert.EqualValues(t, expectedVaultAddress, tokenAccount.PublicKey().ToBytes()) -} - -func TestGetTimelockAccounts_Legacy2022Program(t *testing.T) { - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - expectedStateAddress, expectedStateBump, err := timelock_token_legacy.GetStateAddress(&timelock_token_legacy.GetStateAddressArgs{ - Mint: mintAccount.PublicKey().ToBytes(), - TimeAuthority: subsidizerAccount.PublicKey().ToBytes(), - Nonce: defaultTimelockNonceAccount.PublicKey().ToBytes(), - VaultOwner: ownerAccount.PublicKey().ToBytes(), - UnlockDuration: timelock_token_legacy.DefaultUnlockDuration, - }) - require.NoError(t, err) - - expectedVaultAddress, expectedVaultBump, err := timelock_token_legacy.GetVaultAddress(&timelock_token_legacy.GetVaultAddressArgs{ - State: expectedStateAddress, - }) - require.NoError(t, err) - - actual, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - assert.Equal(t, timelock_token_v1.DataVersionLegacy, actual.DataVersion) - assert.EqualValues(t, expectedStateAddress, actual.State.PublicKey().ToBytes()) - assert.Equal(t, expectedStateBump, actual.StateBump) - assert.EqualValues(t, expectedVaultAddress, actual.Vault.PublicKey().ToBytes()) - assert.Equal(t, expectedVaultBump, actual.VaultBump) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), actual.VaultOwner.PublicKey().ToBytes()) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), actual.TimeAuthority.PublicKey().ToBytes()) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), actual.CloseAuthority.PublicKey().ToBytes()) - assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), actual.Mint.PublicKey().ToBytes()) -} - -func TestIsAccountManagedByCode_TimelockState_Legacy2022Program(t *testing.T) { - ctx := context.Background() - data := code_data.NewTestDataProvider() - - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - - // No record of the account anywhere - result, err := timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.False(t, result) - - // The account is a locked timelock account with Code as the time and close authority - timelockRecord := timelockAccounts.ToDBRecord() - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err = timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.True(t, result) - - timelockRecord.VaultState = timelock_token_v1.StateLocked - timelockRecord.Block += 1 - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err = timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.True(t, result) - - // The timelock account is waiting for timeout - timelockRecord.VaultState = timelock_token_v1.StateWaitingForTimeout - timelockRecord.Block += 1 - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err = timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.False(t, result) - - // The timelock account is unlocked - timelockRecord.VaultState = timelock_token_v1.StateUnlocked - timelockRecord.Block += 1 - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err = timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.False(t, result) -} - -func TestIsAccountManagedByCode_OtherAccounts_Legacy2022Program(t *testing.T) { - ctx := context.Background() - data := code_data.NewTestDataProvider() - - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - require.NoError(t, data.SaveTimelock(ctx, timelockAccounts.ToDBRecord())) - - result, err := timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.True(t, result) - - result, err = timelockAccounts.VaultOwner.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.False(t, result) - - result, err = timelockAccounts.State.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.False(t, result) -} - -func TestIsAccountManagedByCode_TimeAuthority_Legacy2022Program(t *testing.T) { - ctx := context.Background() - data := code_data.NewTestDataProvider() - - subsidizerAccount = newRandomTestAccount(t) - - timeAuthorities := []*Account{ - subsidizerAccount, - newRandomTestAccount(t), - } - - mintAccount := newRandomTestAccount(t) - - for _, timeAuthority := range timeAuthorities { - ownerAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - timelockRecord := timelockAccounts.ToDBRecord() - timelockRecord.TimeAuthority = timeAuthority.PublicKey().ToBase58() - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err := timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.Equal(t, timeAuthority.PublicKey().ToBase58() == subsidizerAccount.PublicKey().ToBase58(), result) - } -} - -func TestIsAccountManagedByCode_CloseAuthority_Legacy2022Program(t *testing.T) { - ctx := context.Background() - data := code_data.NewTestDataProvider() - - subsidizerAccount = newRandomTestAccount(t) - - closeAuthorities := []*Account{ - subsidizerAccount, - newRandomTestAccount(t), - } - - mintAccount := newRandomTestAccount(t) - - for _, closeAuthority := range closeAuthorities { - ownerAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - timelockRecord := timelockAccounts.ToDBRecord() - timelockRecord.CloseAuthority = closeAuthority.PublicKey().ToBase58() - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err := timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.Equal(t, closeAuthority.PublicKey().ToBase58() == subsidizerAccount.PublicKey().ToBase58(), result) - } -} - -func TestIsAccountManagedByCode_DataVersionClosed_Legacy2022Program(t *testing.T) { - ctx := context.Background() - data := code_data.NewTestDataProvider() - - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - timelockRecord := timelockAccounts.ToDBRecord() - timelockRecord.DataVersion = timelock_token_v1.DataVersionClosed - require.NoError(t, data.SaveTimelock(ctx, timelockRecord)) - - result, err := timelockAccounts.Vault.IsManagedByCode(ctx, data) - require.NoError(t, err) - assert.False(t, result) -} - -func TestGetInitializeInstruction_Legacy2022Program(t *testing.T) { - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - - _, err = timelockAccounts.GetInitializeInstruction() - assert.Error(t, err) -} - -func TestGetTransferWithAuthorityInstruction_Legacy2022Program(t *testing.T) { - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - - _, err = timelockAccounts.GetTransferWithAuthorityInstruction(newRandomTestAccount(t), kin.ToQuarks(123)) - assert.Error(t, err) -} - -func TestGetWithdrawInstruction_Legacy2022Program(t *testing.T) { - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - source, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - - destination := newRandomTestAccount(t) - - ixn, err := source.GetWithdrawInstruction(destination) - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_legacy.WithdrawInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, source.StateBump, args.TimelockBump) - - assert.EqualValues(t, source.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, source.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), accounts.VaultOwner) - assert.EqualValues(t, destination.PublicKey().ToBytes(), accounts.Destination) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetBurnDustWithAuthorityInstruction_Legacy2022Program(t *testing.T) { - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - - maxAmount := kin.ToQuarks(1) - - ixn, err := timelockAccounts.GetBurnDustWithAuthorityInstruction(maxAmount) - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_legacy.BurnDustWithAuthorityInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelockAccounts.StateBump, args.TimelockBump) - assert.Equal(t, maxAmount, args.MaxAmount) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, timelockAccounts.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), accounts.VaultOwner) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.TimeAuthority) - assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), accounts.Mint) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetRevokeLockWithAuthorityInstruction_Legacy2022Program(t *testing.T) { - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, mintAccount) - require.NoError(t, err) - - ixn, err := timelockAccounts.GetRevokeLockWithAuthorityInstruction() - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_legacy.RevokeLockWithAuthorityFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelockAccounts.StateBump, args.TimelockBump) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, timelockAccounts.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.TimeAuthority) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetDeactivateInstruction_Legacy2022Program(t *testing.T) { +func TestGetDeactivateInstruction(t *testing.T) { subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) ixn, err := timelockAccounts.GetDeactivateInstruction() @@ -816,12 +363,12 @@ func TestGetDeactivateInstruction_Legacy2022Program(t *testing.T) { assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) } -func TestGetCloseAccountsInstruction_Legacy2022Program(t *testing.T) { +func TestGetCloseAccountsInstruction(t *testing.T) { subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) require.NoError(t, err) ixn, err := timelockAccounts.GetCloseAccountsInstruction() diff --git a/pkg/code/common/owner.go b/pkg/code/common/owner.go index f864be09..2ef1b89c 100644 --- a/pkg/code/common/owner.go +++ b/pkg/code/common/owner.go @@ -9,10 +9,8 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/phone" "github.com/code-payments/code-server/pkg/code/data/timelock" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) var ( @@ -90,12 +88,6 @@ func GetOwnerMetadata(ctx context.Context, data code_data.Provider, owner *Accou // // todo: Needs tests here, but most already exist in account service func GetOwnerManagementState(ctx context.Context, data code_data.Provider, owner *Account) (OwnerManagementState, error) { - legacyPrimary2022Records, err := GetLegacyPrimary2022AccountRecordsIfNotMigrated(ctx, data, owner) - if err != ErrNoPrivacyMigration2022 && err != nil { - return OwnerManagementStateUnknown, err - } - hasLegacyPrimary2022Account := (err == nil) - recordsByType, err := GetLatestTokenAccountRecordsForOwner(ctx, data, owner) if err != nil { return OwnerManagementStateUnknown, err @@ -104,14 +96,11 @@ func GetOwnerManagementState(ctx context.Context, data code_data.Provider, owner // Has an account ever been opened with the owner? If not, the owner is not a Code account. // SubmitIntent guarantees all accounts are opened, so there's no need to do anything more // than an empty check. - if len(recordsByType) == 0 && !hasLegacyPrimary2022Account { + if len(recordsByType) == 0 { return OwnerManagementStateNotFound, nil } // Are all opened accounts managed by Code? If not, the owner is not a Code account. - if hasLegacyPrimary2022Account && !legacyPrimary2022Records.IsManagedByCode(ctx) { - return OwnerManagementStateUnlocked, nil - } for _, batchAccountRecords := range recordsByType { for _, accountRecords := range batchAccountRecords { if accountRecords.IsTimelock() && !accountRecords.IsManagedByCode(ctx) { @@ -197,57 +186,6 @@ func GetLatestCodeTimelockAccountRecordsForOwner(ctx context.Context, data code_ return res, nil } -// GetLegacyPrimary2022AccountRecordsIfNotMigrated gets a faked AccountRecords -// for the LEGACY_PRIMARY_2022 account associated with the provided owner. If -// the account doesn't exist, or was migrated, then ErrNoPrivacyMigration2022 -// is returned. -// -// Note: Legacy Timelock accounts were always Kin accounts -// -// todo: Needs tests here, but most already exist in account service -func GetLegacyPrimary2022AccountRecordsIfNotMigrated(ctx context.Context, data code_data.Provider, owner *Account) (*AccountRecords, error) { - tokenAccount, err := owner.ToTimelockVault(timelock_token_v1.DataVersionLegacy, KinMintAccount) - if err != nil { - return nil, err - } - - timelockRecord, err := data.GetTimelockByVault(ctx, tokenAccount.PublicKey().ToBase58()) - if err == timelock.ErrTimelockNotFound { - return nil, ErrNoPrivacyMigration2022 - } else if err != nil { - return nil, err - } - - // Timelock account is closed, so it doesn't need migrating - if timelockRecord.IsClosed() { - return nil, ErrNoPrivacyMigration2022 - } - - // Client has already submitted an intent to migrate to privacy - _, err = data.GetLatestIntentByInitiatorAndType(ctx, intent.MigrateToPrivacy2022, owner.PublicKey().ToBase58()) - if err == nil { - return nil, ErrNoPrivacyMigration2022 - } else if err != intent.ErrIntentNotFound && err != nil { - return nil, err - } - - // Fake an account info record, since we don't save it for legacy primary 2022 - // accounts, but require it for downstream functions. - accountInfoRecord := &account.Record{ - OwnerAccount: owner.PublicKey().ToBase58(), - AuthorityAccount: owner.PublicKey().ToBase58(), - TokenAccount: timelockRecord.VaultAddress, - MintAccount: KinMintAccount.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_LEGACY_PRIMARY_2022, - Index: 0, - } - - return &AccountRecords{ - General: accountInfoRecord, - Timelock: timelockRecord, - }, nil -} - func (t OwnerType) String() string { switch t { case OwnerTypeUnknown: diff --git a/pkg/code/common/owner_test.go b/pkg/code/common/owner_test.go index 3f08ba7f..50f89c21 100644 --- a/pkg/code/common/owner_test.go +++ b/pkg/code/common/owner_test.go @@ -51,7 +51,7 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { // Later calls intent to OpenAccounts - timelockAccounts, err := owner.GetTimelockAccounts(timelock_token_v1.DataVersion1, coreMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(coreMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -131,7 +131,7 @@ func TestGetOwnerMetadata_RemoteSendGiftCard(t *testing.T) { } require.NoError(t, data.SavePhoneVerification(ctx, verificationRecord)) - timelockAccounts, err := owner.GetTimelockAccounts(timelock_token_v1.DataVersion1, mintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(mintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -180,7 +180,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { {authority1, commonpb.AccountType_BUCKET_1_KIN}, {authority2, commonpb.AccountType_BUCKET_10_KIN}, } { - timelockAccounts, err := authorityAndType.account.GetTimelockAccounts(timelock_token_v1.DataVersion1, coreMintAccount) + timelockAccounts, err := authorityAndType.account.GetTimelockAccounts(coreMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -203,7 +203,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { {authority3, "app1.com"}, {authority4, "app2.com"}, } { - timelockAccounts, err := authorityAndRelationship.account.GetTimelockAccounts(timelock_token_v1.DataVersion1, coreMintAccount) + timelockAccounts, err := authorityAndRelationship.account.GetTimelockAccounts(coreMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() diff --git a/pkg/code/data/timelock/memory/store.go b/pkg/code/data/timelock/memory/store.go index aeaa6c56..042e8f3c 100644 --- a/pkg/code/data/timelock/memory/store.go +++ b/pkg/code/data/timelock/memory/store.go @@ -6,9 +6,9 @@ import ( "sync" "time" + "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/database/query" timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" - "github.com/code-payments/code-server/pkg/code/data/timelock" ) type store struct { @@ -49,11 +49,6 @@ func (s *store) Save(_ context.Context, data *timelock.Record) error { unlockAt = &value } - item.DataVersion = data.DataVersion - - item.CloseAuthority = data.CloseAuthority - item.TimeAuthority = data.TimeAuthority - item.VaultState = data.VaultState item.UnlockAt = unlockAt diff --git a/pkg/code/data/timelock/postgres/model.go b/pkg/code/data/timelock/postgres/model.go index 692af7ff..d77cf5af 100644 --- a/pkg/code/data/timelock/postgres/model.go +++ b/pkg/code/data/timelock/postgres/model.go @@ -22,8 +22,6 @@ const ( type model struct { Id sql.NullInt64 `db:"id"` - DataVersion uint `db:"data_version"` - Address string `db:"address"` Bump uint `db:"bump"` @@ -32,13 +30,7 @@ type model struct { VaultOwner string `db:"vault_owner"` VaultState uint `db:"vault_state"` - TimeAuthority string `db:"time_authority"` - CloseAuthority string `db:"close_authority"` - - Mint string `db:"mint"` - - NumDaysLocked uint `db:"num_days_locked"` - UnlockAt sql.NullInt64 `db:"unlock_at"` + UnlockAt sql.NullInt64 `db:"unlock_at"` Block uint64 `db:"block"` @@ -57,8 +49,6 @@ func toModel(obj *timelock.Record) (*model, error) { } return &model{ - DataVersion: uint(obj.DataVersion), - Address: obj.Address, Bump: uint(obj.Bump), @@ -67,13 +57,7 @@ func toModel(obj *timelock.Record) (*model, error) { VaultOwner: obj.VaultOwner, VaultState: uint(obj.VaultState), - TimeAuthority: obj.TimeAuthority, - CloseAuthority: obj.CloseAuthority, - - Mint: obj.Mint, - - NumDaysLocked: uint(obj.NumDaysLocked), - UnlockAt: unlockAt, + UnlockAt: unlockAt, Block: obj.Block, @@ -91,8 +75,6 @@ func fromModel(obj *model) *timelock.Record { return &timelock.Record{ Id: uint64(obj.Id.Int64), - DataVersion: timelock_token.TimelockDataVersion(obj.DataVersion), - Address: obj.Address, Bump: uint8(obj.Bump), @@ -101,13 +83,7 @@ func fromModel(obj *model) *timelock.Record { VaultOwner: obj.VaultOwner, VaultState: timelock_token.TimelockState(obj.VaultState), - TimeAuthority: obj.TimeAuthority, - CloseAuthority: obj.CloseAuthority, - - Mint: obj.Mint, - - NumDaysLocked: uint8(obj.NumDaysLocked), - UnlockAt: unlockAt, + UnlockAt: unlockAt, Block: obj.Block, @@ -118,16 +94,16 @@ func fromModel(obj *model) *timelock.Record { func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { query := `INSERT INTO ` + tableName + ` - (data_version, address, bump, vault_address, vault_bump, vault_owner, vault_state, time_authority, close_authority, mint, num_days_locked, unlock_at, block, last_updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + (address, bump, vault_address, vault_bump, vault_owner, vault_state, unlock_at, block, last_updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (address) DO UPDATE - SET data_version = $1, vault_state = $7, time_authority = $8, close_authority = $9, unlock_at = $12, block = $13, last_updated_at = $14 - WHERE ` + tableName + `.address = $2 AND ` + tableName + `.vault_address = $4 AND ` + tableName + `.block < $13 + SET vault_state = $6, unlock_at = $7, block = $8, last_updated_at = $9 + WHERE ` + tableName + `.address = $1 AND ` + tableName + `.vault_address = $3 AND ` + tableName + `.block < $8 RETURNING - id, data_version, address, bump, vault_address, vault_bump, vault_owner, vault_state, time_authority, close_authority, mint, num_days_locked, unlock_at, block, last_updated_at` + id, address, bump, vault_address, vault_bump, vault_owner, vault_state, unlock_at, block, last_updated_at` m.LastUpdatedAt = time.Now() @@ -135,8 +111,6 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { ctx, query, - m.DataVersion, - m.Address, m.Bump, @@ -145,12 +119,6 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { m.VaultOwner, m.VaultState, - m.TimeAuthority, - m.CloseAuthority, - - m.Mint, - - m.NumDaysLocked, m.UnlockAt, m.Block, @@ -166,7 +134,7 @@ func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*model, e res := &model{} query := `SELECT - id, data_version, address, bump, vault_address, vault_bump, vault_owner, vault_state, time_authority, close_authority, mint, num_days_locked, unlock_at, block, last_updated_at + id, address, bump, vault_address, vault_bump, vault_owner, vault_state, unlock_at, block, last_updated_at FROM ` + tableName + ` WHERE address = $1 LIMIT 1` @@ -182,7 +150,7 @@ func dbGetByVault(ctx context.Context, db *sqlx.DB, vault string) (*model, error res := &model{} query := `SELECT - id, data_version, address, bump, vault_address, vault_bump, vault_owner, vault_state, time_authority, close_authority, mint, num_days_locked, unlock_at, block, last_updated_at + id, address, bump, vault_address, vault_bump, vault_owner, vault_state, unlock_at, block, last_updated_at FROM ` + tableName + ` WHERE vault_address = $1 LIMIT 1` @@ -203,7 +171,7 @@ func dbGetByVaultBatch(ctx context.Context, db *sqlx.DB, vaults ...string) ([]*m } query := fmt.Sprintf( - `SELECT id, data_version, address, bump, vault_address, vault_bump, vault_owner, vault_state, time_authority, close_authority, mint, num_days_locked, unlock_at, block, last_updated_at + `SELECT id, address, bump, vault_address, vault_bump, vault_owner, vault_state, unlock_at, block, last_updated_at FROM `+tableName+` WHERE vault_address IN (%s)`, strings.Join(individualFilters, ", "), @@ -223,7 +191,7 @@ func dbGetAllByState(ctx context.Context, db *sqlx.DB, state timelock_token.Time res := []*model{} query := `SELECT - id, data_version, address, bump, vault_address, vault_bump, vault_owner, vault_state, time_authority, close_authority, mint, num_days_locked, unlock_at, block, last_updated_at + id, address, bump, vault_address, vault_bump, vault_owner, vault_state, unlock_at, block, last_updated_at FROM ` + tableName + ` WHERE (vault_state = $1) ` diff --git a/pkg/code/data/timelock/postgres/store_test.go b/pkg/code/data/timelock/postgres/store_test.go index 9b8c64d1..27f6f2ab 100644 --- a/pkg/code/data/timelock/postgres/store_test.go +++ b/pkg/code/data/timelock/postgres/store_test.go @@ -22,8 +22,6 @@ const ( CREATE TABLE codewallet__core_timelock( id SERIAL NOT NULL PRIMARY KEY, - data_version INTEGER NOT NULL, - address TEXT NOT NULL, bump INTEGER NOT NULL, @@ -32,12 +30,6 @@ const ( vault_owner TEXT NOT NULL, vault_state INTEGER NOT NULL, - time_authority TEXT NOT NULL, - close_authority TEXT NOT NULL, - - mint TEXT NOT NULL, - - num_days_locked INTEGER NOT NULL, unlock_at INTEGER, block INTEGER NOT NULL, diff --git a/pkg/code/data/timelock/tests/tests.go b/pkg/code/data/timelock/tests/tests.go index 2f92b536..77c586f3 100644 --- a/pkg/code/data/timelock/tests/tests.go +++ b/pkg/code/data/timelock/tests/tests.go @@ -17,7 +17,6 @@ import ( func RunTests(t *testing.T, s timelock.Store, teardown func()) { for _, tf := range []func(t *testing.T, s timelock.Store){ testHappyPath, - testMultiVersionRecords, testBatchedMethods, testGetAllByState, testGetCountByState, @@ -34,8 +33,6 @@ func testHappyPath(t *testing.T, s timelock.Store) { ctx := context.Background() expected := &timelock.Record{ - DataVersion: timelock_token.DataVersion1, - Address: "state", Bump: 254, @@ -44,13 +41,6 @@ func testHappyPath(t *testing.T, s timelock.Store) { VaultOwner: "owner", VaultState: timelock_token.StateUnknown, - TimeAuthority: "time_authority", - CloseAuthority: "close_authority", - - Mint: "mint", - - NumDaysLocked: timelock_token.DefaultNumDaysLocked, - Block: 123456, } cloned := expected.Clone() @@ -82,16 +72,11 @@ func testHappyPath(t *testing.T, s timelock.Store) { previousLastUpdatedTs := expected.LastUpdatedAt - expected.DataVersion = timelock_token.DataVersionClosed - unlockedAt := uint64(time.Now().Unix()) expected.UnlockAt = &unlockedAt expected.VaultState = timelock_token.StateUnlocked - expected.TimeAuthority = "time_authority_attacker" - expected.CloseAuthority = "close_authority_attacker" - // Try to save the record with old blockchain data, which should fail expected.Block = initialBlock - 1 @@ -120,74 +105,6 @@ func testHappyPath(t *testing.T, s timelock.Store) { }) } -func testMultiVersionRecords(t *testing.T, s timelock.Store) { - t.Run("testMultiVersionRecords", func(t *testing.T) { - ctx := context.Background() - - owner := "owner" - - legacyRecord := &timelock.Record{ - DataVersion: timelock_token.DataVersionLegacy, - - Address: "state-legacy", - Bump: 254, - - VaultAddress: "vault-legacy", - VaultBump: 255, - VaultOwner: owner, - VaultState: timelock_token.StateUnknown, - - TimeAuthority: "time_authority", - CloseAuthority: "close_authority", - - Mint: "mint", - - NumDaysLocked: timelock_token.DefaultNumDaysLocked, - - Block: 123456, - } - require.NoError(t, s.Save(ctx, legacyRecord)) - - v1Record := &timelock.Record{ - DataVersion: timelock_token.DataVersion1, - - Address: "state-v1", - Bump: 253, - - VaultAddress: "vault-v1", - VaultBump: 252, - VaultOwner: owner, - VaultState: timelock_token.StateUnknown, - - TimeAuthority: "time_authority", - CloseAuthority: "close_authority", - - Mint: "mint", - - NumDaysLocked: timelock_token.DefaultNumDaysLocked, - - Block: 123456, - } - require.NoError(t, s.Save(ctx, v1Record)) - - actual, err := s.GetByAddress(ctx, legacyRecord.Address) - require.NoError(t, err) - assertEquivalentRecords(t, legacyRecord, actual) - - actual, err = s.GetByVault(ctx, legacyRecord.VaultAddress) - require.NoError(t, err) - assertEquivalentRecords(t, legacyRecord, actual) - - actual, err = s.GetByAddress(ctx, v1Record.Address) - require.NoError(t, err) - assertEquivalentRecords(t, v1Record, actual) - - actual, err = s.GetByVault(ctx, v1Record.VaultAddress) - require.NoError(t, err) - assertEquivalentRecords(t, v1Record, actual) - }) -} - func testBatchedMethods(t *testing.T, s timelock.Store) { t.Run("testBatchedMethods", func(t *testing.T) { ctx := context.Background() @@ -195,8 +112,6 @@ func testBatchedMethods(t *testing.T, s timelock.Store) { var records []*timelock.Record for i := 0; i < 100; i++ { record := &timelock.Record{ - DataVersion: timelock_token.DataVersion1, - Address: fmt.Sprintf("state%d", i), Bump: 254, @@ -205,13 +120,6 @@ func testBatchedMethods(t *testing.T, s timelock.Store) { VaultOwner: fmt.Sprintf("owner%d", i), VaultState: timelock_token.StateUnknown, - TimeAuthority: "time_authority", - CloseAuthority: "close_authority", - - Mint: "mint", - - NumDaysLocked: timelock_token.DefaultNumDaysLocked, - Block: uint64(i), } @@ -250,8 +158,6 @@ func testGetAllByState(t *testing.T, s timelock.Store) { var expected []*timelock.Record for i := 0; i < 100; i++ { record := &timelock.Record{ - DataVersion: timelock_token.DataVersion1, - Address: fmt.Sprintf("state%d", i), Bump: 254, @@ -260,13 +166,6 @@ func testGetAllByState(t *testing.T, s timelock.Store) { VaultOwner: fmt.Sprintf("owner%d", i), VaultState: timelock_token.StateUnknown, - TimeAuthority: "time_authority", - CloseAuthority: "close_authority", - - Mint: "mint", - - NumDaysLocked: timelock_token.DefaultNumDaysLocked, - Block: uint64(i), } @@ -328,8 +227,6 @@ func testGetCountByState(t *testing.T, s timelock.Store) { } { for i := 0; i < int(state); i++ { record := &timelock.Record{ - DataVersion: timelock_token.DataVersion1, - Address: fmt.Sprintf("state-%s-%d", state, i), Bump: 254, @@ -337,13 +234,6 @@ func testGetCountByState(t *testing.T, s timelock.Store) { VaultBump: 255, VaultOwner: fmt.Sprintf("owner-%s-%d", state, i), VaultState: state, - - TimeAuthority: "time_authority", - CloseAuthority: "close_authority", - - Mint: "mint", - - NumDaysLocked: timelock_token.DefaultNumDaysLocked, } require.NoError(t, s.Save(ctx, record)) @@ -357,8 +247,6 @@ func testGetCountByState(t *testing.T, s timelock.Store) { } func assertEquivalentRecords(t *testing.T, obj1, obj2 *timelock.Record) { - assert.Equal(t, obj1.DataVersion, obj2.DataVersion) - assert.Equal(t, obj1.Address, obj2.Address) assert.Equal(t, obj1.Bump, obj2.Bump) @@ -367,12 +255,6 @@ func assertEquivalentRecords(t *testing.T, obj1, obj2 *timelock.Record) { assert.Equal(t, obj1.VaultOwner, obj2.VaultOwner) assert.Equal(t, obj1.VaultState, obj2.VaultState) - assert.Equal(t, obj1.TimeAuthority, obj2.TimeAuthority) - assert.Equal(t, obj1.CloseAuthority, obj2.CloseAuthority) - - assert.Equal(t, obj1.Mint, obj2.Mint) - - assert.Equal(t, obj1.NumDaysLocked, obj2.NumDaysLocked) assert.EqualValues(t, obj1.UnlockAt, obj2.UnlockAt) assert.Equal(t, obj1.Block, obj2.Block) diff --git a/pkg/code/data/timelock/timelock.go b/pkg/code/data/timelock/timelock.go index 30b5a8f8..701d07d8 100644 --- a/pkg/code/data/timelock/timelock.go +++ b/pkg/code/data/timelock/timelock.go @@ -3,10 +3,8 @@ package timelock import ( "time" - "github.com/mr-tron/base58" "github.com/pkg/errors" - timelock_token_legacy "github.com/code-payments/code-server/pkg/solana/timelock/legacy_2022" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) @@ -16,16 +14,12 @@ var ( ErrStaleTimelockState = errors.New("timelock state is stale") ) -// Based off of https://github.com/code-payments/code-program-library/blob/main/timelock-token/programs/timelock-token/src/state.rs +// Time/close authorities and lock duration are configured at the VM level // -// This record supports both the legacy 2022 (pre-privacy) and v1 versions of -// the timelock program, since they're easily interchangeable. All legacy fields -// will be converted accordingly, or dropped if never used. +// todo: Assumes a single VM. type Record struct { Id uint64 - DataVersion timelock_token_v1.TimelockDataVersion - Address string Bump uint8 @@ -34,13 +28,7 @@ type Record struct { VaultOwner string VaultState timelock_token_v1.TimelockState - TimeAuthority string - CloseAuthority string - - Mint string - - NumDaysLocked uint8 - UnlockAt *uint64 + UnlockAt *uint64 Block uint64 @@ -66,94 +54,6 @@ func (r *Record) ExistsOnBlockchain() bool { return r.VaultState != timelock_token_v1.StateUnknown && r.VaultState != timelock_token_v1.StateClosed } -func (r *Record) UpdateFromV1ProgramAccount(data *timelock_token_v1.TimelockAccount, block uint64) error { - // Avoid updates looking backwards in blockchain history - - if block <= r.Block { - return ErrStaleTimelockState - } - - // Check the expected data version. If we encounter a closed version, simply - // update the record's data version. Expect all other data to be garbage, so - // don't include it. Ideally this should never happen, but we need to be - // extra safe in the event of an unknown attack vector. This can directly - // affect our ability to manage an account's funds. - - if data.DataVersion == timelock_token_v1.DataVersionClosed { - r.DataVersion = timelock_token_v1.DataVersionClosed - r.Block = block - return nil - } - - if data.DataVersion != timelock_token_v1.DataVersion1 { - return errors.New("timelock data version must be 1") - } - - // It's now safe to update the record - - var unlockAt *uint64 - if data.UnlockAt != nil { - value := *data.UnlockAt - unlockAt = &value - } - - // These 2 fields should never change under ideal circumstances, but we need - // to be extra safe in the event of an unknown attack vector. These directly - // affect our ability to manage an account's funds. - r.TimeAuthority = base58.Encode(data.TimeAuthority) - r.CloseAuthority = base58.Encode(data.CloseAuthority) - - r.VaultState = data.VaultState - r.UnlockAt = unlockAt - - r.Block = block - - return nil -} - -func (r *Record) UpdateFromLegacy2022ProgramAccount(data *timelock_token_legacy.TimelockAccount, block uint64) error { - // Avod updates looking backwards in blockchain history - - if block <= r.Block { - return ErrStaleTimelockState - } - - // Check the expected data version - - // Handle init_offset similarly to how we handle DataVersionClosed in the v1 - // update method. - if data.InitOffset != 0 { - r.DataVersion = timelock_token_v1.DataVersionClosed - r.Block = block - return nil - } - - if data.DataVersion != timelock_token_legacy.TimelockDataVersion(timelock_token_v1.DataVersionLegacy) { - return errors.New("timelock data version must be legacy") - } - - // It's now safe to update the record - - var unlockAt *uint64 - if data.UnlockAt != nil { - value := *data.UnlockAt - unlockAt = &value - } - - // These 2 fields should never change under ideal circumstances, but we need - // to be extra safe in the event of an unknown attack vector. These directly - // affect our ability to manage an account's funds. - r.TimeAuthority = base58.Encode(data.TimeAuthority) - r.CloseAuthority = base58.Encode(data.CloseAuthority) - - r.VaultState = timelock_token_v1.TimelockState(data.VaultState) - r.UnlockAt = unlockAt - - r.Block = block - - return nil -} - func (r *Record) Clone() *Record { var unlockAt *uint64 if r.UnlockAt != nil { @@ -164,8 +64,6 @@ func (r *Record) Clone() *Record { return &Record{ Id: r.Id, - DataVersion: r.DataVersion, - Address: r.Address, Bump: r.Bump, @@ -174,13 +72,7 @@ func (r *Record) Clone() *Record { VaultOwner: r.VaultOwner, VaultState: r.VaultState, - TimeAuthority: r.TimeAuthority, - CloseAuthority: r.CloseAuthority, - - Mint: r.Mint, - - NumDaysLocked: r.NumDaysLocked, - UnlockAt: unlockAt, + UnlockAt: unlockAt, Block: r.Block, @@ -197,8 +89,6 @@ func (r *Record) CopyTo(dst *Record) { dst.Id = r.Id - dst.DataVersion = r.DataVersion - dst.Address = r.Address dst.Bump = r.Bump @@ -207,12 +97,6 @@ func (r *Record) CopyTo(dst *Record) { dst.VaultOwner = r.VaultOwner dst.VaultState = r.VaultState - dst.TimeAuthority = r.TimeAuthority - dst.CloseAuthority = r.CloseAuthority - - dst.Mint = r.Mint - - dst.NumDaysLocked = r.NumDaysLocked dst.UnlockAt = unlockAt dst.Block = r.Block @@ -225,14 +109,6 @@ func (r *Record) Validate() error { return errors.New("record is nil") } - switch r.DataVersion { - case timelock_token_v1.DataVersionLegacy, - timelock_token_v1.DataVersion1, - timelock_token_v1.DataVersionClosed: - default: - return errors.New("invalid timelock data version") - } - if len(r.Address) == 0 { return errors.New("state address is required") } @@ -245,21 +121,5 @@ func (r *Record) Validate() error { return errors.New("vault owner is required") } - if len(r.TimeAuthority) == 0 { - return errors.New("time authority is required") - } - - if len(r.CloseAuthority) == 0 { - return errors.New("close authority is required") - } - - if len(r.Mint) == 0 { - return errors.New("mint is required") - } - - if r.NumDaysLocked != timelock_token_v1.DefaultNumDaysLocked { - return errors.Errorf("num days locked must be %d days", timelock_token_v1.DefaultNumDaysLocked) - } - return nil } From ae03fb85e33212147a6279984a90cb32f9eb42a4 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Tue, 30 Jul 2024 16:33:33 -0400 Subject: [PATCH 17/79] Round out virtual instructions with signature updates and adding missing implementations (#161) --- pkg/solana/cvm/types_opcode.go | 2 + pkg/solana/cvm/types_signature.go | 22 ++++++ pkg/solana/cvm/virtual_instruction.go | 5 -- ...al_instructions_relay_transfer_external.go | 6 +- ...al_instructions_relay_transfer_internal.go | 6 +- ...rtual_instructions_timelock_close_empty.go | 69 +++++++++++++++++++ ...instructions_timelock_transfer_external.go | 11 +-- ...instructions_timelock_transfer_internal.go | 11 +-- ...al_instructions_timelock_transfer_relay.go | 11 +-- 9 files changed, 120 insertions(+), 23 deletions(-) create mode 100644 pkg/solana/cvm/types_signature.go create mode 100644 pkg/solana/cvm/virtual_instructions_timelock_close_empty.go diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index 321c16ed..b1cf5f1a 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -9,6 +9,8 @@ const ( OpcodeTransferWithCommitmentInternal Opcode = 52 OpcodeTransferWithCommitmentExternal Opcode = 53 + + OpcodeCompoundCloseEmptyAccount Opcode = 60 ) func putOpcode(dst []byte, v Opcode, offset *int) { diff --git a/pkg/solana/cvm/types_signature.go b/pkg/solana/cvm/types_signature.go new file mode 100644 index 00000000..d918e48f --- /dev/null +++ b/pkg/solana/cvm/types_signature.go @@ -0,0 +1,22 @@ +package cvm + +import ( + "github.com/mr-tron/base58" +) + +const SignatureSize = 64 + +type Signature [SignatureSize]byte + +func (s Signature) String() string { + return base58.Encode(s[:]) +} + +func putSignature(dst []byte, v Signature, offset *int) { + copy(dst[*offset:], v[:]) + *offset += SignatureSize +} +func getSignature(src []byte, dst *Signature, offset *int) { + copy(dst[:], src[*offset:]) + *offset += SignatureSize +} diff --git a/pkg/solana/cvm/virtual_instruction.go b/pkg/solana/cvm/virtual_instruction.go index 35ef5926..d051f2d8 100644 --- a/pkg/solana/cvm/virtual_instruction.go +++ b/pkg/solana/cvm/virtual_instruction.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "github.com/code-payments/code-server/pkg/solana" - solana_ed25519 "github.com/code-payments/code-server/pkg/solana/ed25519" "github.com/code-payments/code-server/pkg/solana/memo" "github.com/code-payments/code-server/pkg/solana/system" ) @@ -46,10 +45,6 @@ func NewVirtualInstruction( } } -func (i VirtualInstruction) GetEd25519Instruction(user ed25519.PrivateKey) solana.Instruction { - return solana_ed25519.Instruction(user, i.Hash[:]) -} - func getTxnMessageHash(txn solana.Transaction) Hash { msg := txn.Message.Marshal() h := sha256.New() diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go index a26839bb..1caa0616 100644 --- a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go +++ b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go @@ -5,14 +5,14 @@ import ( ) const ( - RelayTransferExternalVirtrualInstructionDataSize = (8 + // amount + RelayTransferExternalVirtrualInstructionDataSize = (4 + // amount HashSize + // transcript HashSize + // recent_root HashSize) // commitment ) type RelayTransferExternalVirtualInstructionArgs struct { - Amount uint64 + Amount uint32 Transcript Hash RecentRoot Hash Commitment Hash @@ -29,7 +29,7 @@ func NewRelayTransferExternalVirtualInstructionCtor( var offset int data := make([]byte, RelayTransferExternalVirtrualInstructionDataSize) - putUint64(data, args.Amount, &offset) + putUint32(data, args.Amount, &offset) putHash(data, args.Transcript, &offset) putHash(data, args.RecentRoot, &offset) putHash(data, args.Commitment, &offset) diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go index 29fb5347..231243bf 100644 --- a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go +++ b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go @@ -5,14 +5,14 @@ import ( ) const ( - RelayTransferInternalVirtrualInstructionDataSize = (8 + // amount + RelayTransferInternalVirtrualInstructionDataSize = (4 + // amount HashSize + // transcript HashSize + // recent_root HashSize) // commitment ) type RelayTransferInternalVirtualInstructionArgs struct { - Amount uint64 + Amount uint32 Transcript Hash RecentRoot Hash Commitment Hash @@ -29,7 +29,7 @@ func NewRelayTransferInternalVirtualInstructionCtor( var offset int data := make([]byte, RelayTransferInternalVirtrualInstructionDataSize) - putUint64(data, args.Amount, &offset) + putUint32(data, args.Amount, &offset) putHash(data, args.Transcript, &offset) putHash(data, args.RecentRoot, &offset) putHash(data, args.Commitment, &offset) diff --git a/pkg/solana/cvm/virtual_instructions_timelock_close_empty.go b/pkg/solana/cvm/virtual_instructions_timelock_close_empty.go new file mode 100644 index 00000000..72b7f826 --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_timelock_close_empty.go @@ -0,0 +1,69 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" +) + +const ( + TimelockCloseEmptyVirtrualInstructionDataSize = (SignatureSize + // signature + 4) // max_amount +) + +type TimelockCloseEmptyVirtualInstructionArgs struct { + TimelockBump uint8 + MaxAmount uint32 + Signature Signature +} + +type TimelockCloseEmptyVirtualInstructionAccounts struct { + VmAuthority ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + VirtualTimelockVault ed25519.PublicKey + Owner ed25519.PublicKey + Mint ed25519.PublicKey +} + +func NewTimelockCloseEmptyVirtualInstructionCtor( + accounts *TimelockCloseEmptyVirtualInstructionAccounts, + args *TimelockCloseEmptyVirtualInstructionArgs, +) VirtualInstructionCtor { + return func() (Opcode, []solana.Instruction, []byte) { + var offset int + data := make([]byte, TimelockCloseEmptyVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + putUint32(data, args.MaxAmount, &offset) + + ixns := []solana.Instruction{ + timelock_token.NewBurnDustWithAuthorityInstruction( + &timelock_token.BurnDustWithAuthorityInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + VaultOwner: accounts.Owner, + TimeAuthority: accounts.VmAuthority, + Mint: accounts.Mint, + Payer: accounts.VmAuthority, + }, + &timelock_token.BurnDustWithAuthorityInstructionArgs{ + TimelockBump: args.TimelockBump, + MaxAmount: uint64(args.MaxAmount), + }, + ).ToLegacyInstruction(), + timelock_token.NewCloseAccountsInstruction( + &timelock_token.CloseAccountsInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + CloseAuthority: accounts.VmAuthority, + Payer: accounts.VmAuthority, + }, + &timelock_token.CloseAccountsInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + } + + return OpcodeCompoundCloseEmptyAccount, ixns, data + } +} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go index 74a744f4..a2de3eaf 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go @@ -8,12 +8,14 @@ import ( ) const ( - TimelockTransferExternalVirtrualInstructionDataSize = 8 // amount + TimelockTransferExternalVirtrualInstructionDataSize = (SignatureSize + // signature + 4) // amount ) type TimelockTransferExternalVirtualInstructionArgs struct { TimelockBump uint8 - Amount uint64 + Amount uint32 + Signature Signature } type TimelockTransferExternalVirtualInstructionAccounts struct { @@ -31,7 +33,8 @@ func NewTimelockTransferExternalVirtualInstructionCtor( return func() (Opcode, []solana.Instruction, []byte) { var offset int data := make([]byte, TimelockTransferExternalVirtrualInstructionDataSize) - putUint64(data, args.Amount, &offset) + putSignature(data, args.Signature, &offset) + putUint32(data, args.Amount, &offset) ixns := []solana.Instruction{ newKreMemoIxn(), @@ -46,7 +49,7 @@ func NewTimelockTransferExternalVirtualInstructionCtor( }, &timelock_token.TransferWithAuthorityInstructionArgs{ TimelockBump: args.TimelockBump, - Amount: args.Amount, + Amount: uint64(args.Amount), }, ).ToLegacyInstruction(), } diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go index d662b897..99910305 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go @@ -8,12 +8,14 @@ import ( ) const ( - TimelockTransferInternalVirtrualInstructionDataSize = 8 // amount + TimelockTransferInternalVirtrualInstructionDataSize = (SignatureSize + // signature + 4) // amount ) type TimelockTransferInternalVirtualInstructionArgs struct { TimelockBump uint8 - Amount uint64 + Amount uint32 + Signature Signature } type TimelockTransferInternalVirtualInstructionAccounts struct { @@ -31,7 +33,8 @@ func NewTimelockTransferInternalVirtualInstructionCtor( return func() (Opcode, []solana.Instruction, []byte) { var offset int data := make([]byte, TimelockTransferInternalVirtrualInstructionDataSize) - putUint64(data, args.Amount, &offset) + putSignature(data, args.Signature, &offset) + putUint32(data, args.Amount, &offset) ixns := []solana.Instruction{ newKreMemoIxn(), @@ -46,7 +49,7 @@ func NewTimelockTransferInternalVirtualInstructionCtor( }, &timelock_token.TransferWithAuthorityInstructionArgs{ TimelockBump: args.TimelockBump, - Amount: args.Amount, + Amount: uint64(args.Amount), }, ).ToLegacyInstruction(), } diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go index 2766a4ca..d5e15d8c 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go @@ -8,12 +8,14 @@ import ( ) const ( - TimelockTransferRelayVirtrualInstructionDataSize = 8 // amount + TimelockTransferRelayVirtrualInstructionDataSize = (SignatureSize + // signature + 4) // amount ) type TimelockTransferRelayVirtualInstructionArgs struct { TimelockBump uint8 - Amount uint64 + Amount uint32 + Signature Signature } type TimelockTransferRelayVirtualInstructionAccounts struct { @@ -31,7 +33,8 @@ func NewTimelockTransferRelayVirtualInstructionCtor( return func() (Opcode, []solana.Instruction, []byte) { var offset int data := make([]byte, TimelockTransferRelayVirtrualInstructionDataSize) - putUint64(data, args.Amount, &offset) + putSignature(data, args.Signature, &offset) + putUint32(data, args.Amount, &offset) ixns := []solana.Instruction{ newKreMemoIxn(), @@ -46,7 +49,7 @@ func NewTimelockTransferRelayVirtualInstructionCtor( }, &timelock_token.TransferWithAuthorityInstructionArgs{ TimelockBump: args.TimelockBump, - Amount: args.Amount, + Amount: uint64(args.Amount), }, ).ToLegacyInstruction(), } From 0685b2056738d7c57bf68b24a9e914544d4fb304 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 31 Jul 2024 09:02:47 -0400 Subject: [PATCH 18/79] Implement virtual instruction for closing Timelock account with balance (#162) --- pkg/solana/cvm/types_opcode.go | 3 +- ...ons_timelock_close_account_with_balance.go | 87 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 pkg/solana/cvm/virtual_instructions_timelock_close_account_with_balance.go diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index b1cf5f1a..792b682b 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -10,7 +10,8 @@ const ( OpcodeTransferWithCommitmentInternal Opcode = 52 OpcodeTransferWithCommitmentExternal Opcode = 53 - OpcodeCompoundCloseEmptyAccount Opcode = 60 + OpcodeCompoundCloseEmptyAccount Opcode = 60 + OpcodeCompoundCloseAccountWithBalance Opcode = 61 ) func putOpcode(dst []byte, v Opcode, offset *int) { diff --git a/pkg/solana/cvm/virtual_instructions_timelock_close_account_with_balance.go b/pkg/solana/cvm/virtual_instructions_timelock_close_account_with_balance.go new file mode 100644 index 00000000..3840e04b --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_timelock_close_account_with_balance.go @@ -0,0 +1,87 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" +) + +const ( + TimelockCloseAccountWithBalanceVirtrualInstructionDataSize = SignatureSize // signature +) + +type TimelockCloseAccountWithBalanceVirtualInstructionArgs struct { + TimelockBump uint8 + Signature Signature +} + +type TimelockCloseAccountWithBalanceVirtualInstructionAccounts struct { + VmAuthority ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + VirtualTimelockVault ed25519.PublicKey + Destination ed25519.PublicKey + Owner ed25519.PublicKey + Mint ed25519.PublicKey +} + +func NewTimelockCloseAccountWithBalanceVirtualInstructionCtor( + accounts *TimelockCloseAccountWithBalanceVirtualInstructionAccounts, + args *TimelockCloseAccountWithBalanceVirtualInstructionArgs, +) VirtualInstructionCtor { + return func() (Opcode, []solana.Instruction, []byte) { + var offset int + data := make([]byte, TimelockCloseAccountWithBalanceVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + + ixns := []solana.Instruction{ + newKreMemoIxn(), + timelock_token.NewRevokeLockWithAuthorityInstruction( + &timelock_token.RevokeLockWithAuthorityInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + TimeAuthority: accounts.VmAuthority, + Payer: accounts.VmAuthority, + }, + &timelock_token.RevokeLockWithAuthorityInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + timelock_token.NewDeactivateInstruction( + &timelock_token.DeactivateInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + VaultOwner: accounts.Owner, + Payer: accounts.VmAuthority, + }, + &timelock_token.DeactivateInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + timelock_token.NewWithdrawInstruction( + &timelock_token.WithdrawInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + VaultOwner: accounts.Owner, + Destination: accounts.Destination, + Payer: accounts.VmAuthority, + }, + &timelock_token.WithdrawInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + timelock_token.NewCloseAccountsInstruction( + &timelock_token.CloseAccountsInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + CloseAuthority: accounts.VmAuthority, + Payer: accounts.VmAuthority, + }, + &timelock_token.CloseAccountsInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + } + + return OpcodeCompoundCloseAccountWithBalance, ixns, data + } +} From 9986ce60a1b9482477e0aa7b03d95eb0944cafb0 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Fri, 2 Aug 2024 10:49:24 -0400 Subject: [PATCH 19/79] VM sequencer (#159) * Use simplistic fulfillment model for virtual instructions * Handle virtual nonce state in sequencer service due to fulfillment state changes * Remove scheduler handlers for deprecated commitment management fulfillments * Update commitment state flows in sequencer * Construct new on demand transaction for initializing Timelock account * Construct new on demand transaction for treasury advances * Fix some worker tests * Fix commitment checks and comments in private transfer fulfillment handlers * Disable most sequencer tests until we have something rounded out for the VM * Fix errors due to vixn updates * Treasury advance no longer needs KRE memo * Update all transaction construction logic for VM * Fix additional memo in transaction construction of closing account with balance * Add support for compression in place of closing * Update virtual instruction set with latest VM changes * Get rid of closing dormant account action and fulfillment handlers on the sequencer * CloseEmptyTimelockAccount is now an on demand compression transaction * Add sequencer logic for closing commitment account through compression * Remove unlock PDA from exec instruction * Create close commitment fulfillment when commitment account is about to be closed --- pkg/code/async/account/gift_card.go | 3 + pkg/code/async/account/testutil.go | 3 +- pkg/code/async/commitment/testutil.go | 5 +- pkg/code/async/commitment/transaction.go | 47 ++ pkg/code/async/commitment/worker.go | 8 +- pkg/code/async/nonce/pool.go | 10 +- pkg/code/async/sequencer/action_handler.go | 29 +- .../async/sequencer/action_handler_test.go | 25 +- pkg/code/async/sequencer/commitment.go | 27 +- .../async/sequencer/fulfillment_handler.go | 589 +++--------------- .../sequencer/fulfillment_handler_test.go | 42 +- .../async/sequencer/intent_handler_test.go | 23 +- pkg/code/async/sequencer/scheduler_test.go | 40 +- pkg/code/async/sequencer/timelock.go | 18 +- pkg/code/async/sequencer/utils.go | 85 ++- pkg/code/async/sequencer/utils_test.go | 2 + pkg/code/async/sequencer/worker_test.go | 2 + pkg/code/balance/calculator_test.go | 40 +- pkg/code/common/account.go | 64 +- pkg/code/common/account_test.go | 75 ++- pkg/code/common/owner_test.go | 11 +- pkg/code/common/subsidizer.go | 2 +- pkg/code/data/action/action.go | 4 +- pkg/code/data/fulfillment/fulfillment.go | 51 +- pkg/code/data/fulfillment/memory/store.go | 31 +- pkg/code/data/fulfillment/postgres/model.go | 101 ++- pkg/code/data/fulfillment/postgres/store.go | 12 +- .../data/fulfillment/postgres/store_test.go | 4 + pkg/code/data/fulfillment/store.go | 3 + pkg/code/data/fulfillment/tests/tests.go | 24 +- pkg/code/data/internal.go | 4 + pkg/code/transaction/transaction.go | 284 ++++++--- pkg/solana/cvm/address.go | 16 + .../instructions_system_account_compress.go | 72 +++ pkg/solana/cvm/instructions_vm_exec.go | 17 +- .../cvm/instructions_vm_storage_init.go | 84 +++ pkg/solana/cvm/types_opcode.go | 15 +- ...al_instructions_relay_transfer_external.go | 2 +- ...al_instructions_relay_transfer_internal.go | 2 +- ...rtual_instructions_timelock_close_empty.go | 69 -- ...instructions_timelock_transfer_external.go | 2 +- ...instructions_timelock_transfer_internal.go | 2 +- ...al_instructions_timelock_transfer_relay.go | 2 +- ...nstructions_timelock_withdraw_external.go} | 16 +- ...instructions_timelock_withdraw_internal.go | 87 +++ 45 files changed, 1086 insertions(+), 968 deletions(-) create mode 100644 pkg/code/async/commitment/transaction.go create mode 100644 pkg/solana/cvm/instructions_system_account_compress.go create mode 100644 pkg/solana/cvm/instructions_vm_storage_init.go delete mode 100644 pkg/solana/cvm/virtual_instructions_timelock_close_empty.go rename pkg/solana/cvm/{virtual_instructions_timelock_close_account_with_balance.go => virtual_instructions_timelock_withdraw_external.go} (80%) create mode 100644 pkg/solana/cvm/virtual_instructions_timelock_withdraw_internal.go diff --git a/pkg/code/async/account/gift_card.go b/pkg/code/async/account/gift_card.go index 37e37ba1..4ad292dc 100644 --- a/pkg/code/async/account/gift_card.go +++ b/pkg/code/async/account/gift_card.go @@ -29,6 +29,9 @@ import ( "github.com/code-payments/code-server/pkg/retry" ) +// todo: Is this even relevant anymore with the VM? If so, we need new logic because +// closing dormant accounts is no longer a thing with the VM. + const ( giftCardAutoReturnIntentPrefix = "auto-return-gc-" giftCardExpiry = 24 * time.Hour diff --git a/pkg/code/async/account/testutil.go b/pkg/code/async/account/testutil.go index b740f5b8..19f285ca 100644 --- a/pkg/code/async/account/testutil.go +++ b/pkg/code/async/account/testutil.go @@ -60,9 +60,10 @@ func setup(t *testing.T) *testEnv { } func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *testGiftCard { + vm := testutil.NewRandomAccount(t) authority := testutil.NewRandomAccount(t) - timelockAccounts, err := authority.GetTimelockAccounts(common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(vm, common.KinMintAccount) require.NoError(t, err) accountInfoRecord := &account.Record{ diff --git a/pkg/code/async/commitment/testutil.go b/pkg/code/async/commitment/testutil.go index b90da2fa..5a039124 100644 --- a/pkg/code/async/commitment/testutil.go +++ b/pkg/code/async/commitment/testutil.go @@ -102,6 +102,9 @@ func setup(t *testing.T) testEnv { } func (e testEnv) simulateCommitment(t *testing.T, recentRoot string, state commitment.State) *commitment.Record { + vm, err := common.NewAccountFromPublicKeyString(e.treasuryPool.Vm) + require.NoError(t, err) + commitmentRecord := &commitment.Record{ Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), @@ -122,7 +125,7 @@ func (e testEnv) simulateCommitment(t *testing.T, recentRoot string, state commi require.NoError(t, e.data.SaveCommitment(e.ctx, commitmentRecord)) owner := testutil.NewRandomAccount(t) - timelockAccounts, err := owner.GetTimelockAccounts(common.KinMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(vm, common.KinMintAccount) require.NoError(t, err) intentRecord := &intent.Record{ diff --git a/pkg/code/async/commitment/transaction.go b/pkg/code/async/commitment/transaction.go new file mode 100644 index 00000000..0031e1bd --- /dev/null +++ b/pkg/code/async/commitment/transaction.go @@ -0,0 +1,47 @@ +package async_commitment + +import ( + "context" + "math" + + "github.com/code-payments/code-server/pkg/code/data/action" + "github.com/code-payments/code-server/pkg/code/data/commitment" + "github.com/code-payments/code-server/pkg/code/data/fulfillment" +) + +func (p *service) injectCloseCommitmentFulfillment(ctx context.Context, commitmentRecord *commitment.Record) error { + // Idempotency check to ensure we don't double up on fulfillments + _, err := p.data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.CloseCommitment, commitmentRecord.Intent, commitmentRecord.ActionId) + if err == nil { + return nil + } else if err != nil && err != fulfillment.ErrFulfillmentNotFound { + return err + } + + intentRecord, err := p.data.GetIntent(ctx, commitmentRecord.Intent) + if err != nil { + return err + } + + // Transaction is created on demand at time of scheduling + fulfillmentRecord := &fulfillment.Record{ + Intent: intentRecord.IntentId, + IntentType: intentRecord.IntentType, + + ActionId: commitmentRecord.ActionId, + ActionType: action.PrivateTransfer, + + FulfillmentType: fulfillment.CloseCommitment, + + Source: commitmentRecord.Address, + + IntentOrderingIndex: uint64(math.MaxInt64), + ActionOrderingIndex: 0, + FulfillmentOrderingIndex: 0, + + DisableActiveScheduling: false, + + State: fulfillment.StateUnknown, + } + return p.data.PutAllFulfillments(ctx, fulfillmentRecord) +} diff --git a/pkg/code/async/commitment/worker.go b/pkg/code/async/commitment/worker.go index 80652c28..f6d72ffc 100644 --- a/pkg/code/async/commitment/worker.go +++ b/pkg/code/async/commitment/worker.go @@ -122,6 +122,11 @@ func (p *service) handleOpen(ctx context.Context, record *commitment.Record) err } if shouldClose { + err = p.injectCloseCommitmentFulfillment(ctx, record) + if err != nil { + return err + } + return markCommitmentAsClosing(ctx, p.data, record.Intent, record.ActionId) } @@ -205,8 +210,7 @@ func (p *service) shouldCloseCommitment(ctx context.Context, commitmentRecord *c return false, nil } - // todo: There isn't a way to close commitments yet in the VM - return false, nil + return true, nil } func (p *service) maybeMarkCommitmentForGC(ctx context.Context, commitmentRecord *commitment.Record) error { diff --git a/pkg/code/async/nonce/pool.go b/pkg/code/async/nonce/pool.go index 3a8c3195..548957c6 100644 --- a/pkg/code/async/nonce/pool.go +++ b/pkg/code/async/nonce/pool.go @@ -201,7 +201,15 @@ func (p *service) handleReleased(ctx context.Context, record *nonce.Record) erro return err } case nonce.EnvironmentCvm: - return errors.New("todo: implement the process of getting submitted transaction from virtual instruction") + fulfillmentRecord, err := p.data.GetFulfillmentByVirtualSignature(ctx, record.Signature) + if err != nil { + return err + } + + txn, err = p.getTransaction(ctx, *fulfillmentRecord.Signature) + if err != nil { + return err + } } // Sanity check the Solana transaction is in a finalized or failed state diff --git a/pkg/code/async/sequencer/action_handler.go b/pkg/code/async/sequencer/action_handler.go index ccabf1d9..f23b25d1 100644 --- a/pkg/code/async/sequencer/action_handler.go +++ b/pkg/code/async/sequencer/action_handler.go @@ -71,32 +71,6 @@ func (h *CloseEmptyAccountActionHandler) OnFulfillmentStateChange(ctx context.Co return nil } -type CloseDormantAccountActionHandler struct { - data code_data.Provider -} - -func NewCloseDormantAccountActionHandler(data code_data.Provider) ActionHandler { - return &CloseDormantAccountActionHandler{ - data: data, - } -} - -func (h *CloseDormantAccountActionHandler) OnFulfillmentStateChange(ctx context.Context, fulfillmentRecord *fulfillment.Record, newState fulfillment.State) error { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseDormantTimelockAccount { - return errors.New("unexpected fulfillment type") - } - - if newState == fulfillment.StateConfirmed { - return markActionConfirmed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - } - - if newState == fulfillment.StateFailed { - return markActionFailed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - } - - return nil -} - type NoPrivacyTransferActionHandler struct { data code_data.Provider } @@ -177,7 +151,7 @@ func (h *PrivateTransferActionHandler) OnFulfillmentStateChange(ctx context.Cont if newState == fulfillment.StateFailed { return markActionFailed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) } - case fulfillment.InitializeCommitmentProof, fulfillment.UploadCommitmentProof, fulfillment.VerifyCommitmentProof, fulfillment.OpenCommitmentVault, fulfillment.CloseCommitmentVault: + case fulfillment.CloseCommitment: // Don't care about commitment states. These are managed elsewhere. return nil default: @@ -283,7 +257,6 @@ func getActionHandlers(data code_data.Provider) map[action.Type]ActionHandler { handlersByType := make(map[action.Type]ActionHandler) handlersByType[action.OpenAccount] = NewOpenAccountActionHandler(data) handlersByType[action.CloseEmptyAccount] = NewCloseEmptyAccountActionHandler(data) - handlersByType[action.CloseDormantAccount] = NewCloseDormantAccountActionHandler(data) handlersByType[action.NoPrivacyTransfer] = NewNoPrivacyTransferActionHandler(data) handlersByType[action.NoPrivacyWithdraw] = NewNoPrivacyWithdrawActionHandler(data) handlersByType[action.PrivateTransfer] = NewPrivateTransferActionHandler(data) diff --git a/pkg/code/async/sequencer/action_handler_test.go b/pkg/code/async/sequencer/action_handler_test.go index ac018a31..592dbec9 100644 --- a/pkg/code/async/sequencer/action_handler_test.go +++ b/pkg/code/async/sequencer/action_handler_test.go @@ -1,25 +1,8 @@ package async_sequencer -import ( - "context" - "fmt" - "math/rand" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/testutil" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" -) +// todo: fix tests once sequencer is rounded out for the vm + +/* func TestOpenAccountActionHandler_TransitionToStateConfirmed(t *testing.T) { env := setupActionHandlerTestEnv(t) @@ -664,3 +647,5 @@ func getFirstFulfillmentOfType(t *testing.T, records []*fulfillment.Record, fulf require.Fail(t, "fulfillment with type not found") return nil } + +*/ diff --git a/pkg/code/async/sequencer/commitment.go b/pkg/code/async/sequencer/commitment.go index 75a03380..d2cabc9d 100644 --- a/pkg/code/async/sequencer/commitment.go +++ b/pkg/code/async/sequencer/commitment.go @@ -30,26 +30,6 @@ func markCommitmentPayingDestination(ctx context.Context, data code_data.Provide return data.SaveCommitment(ctx, commitmentRecord) } -func markCommitmentReadyToOpen(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return err - } - - if commitmentRecord.State == commitment.StateReadyToOpen { - return nil - } - - if commitmentRecord.State != commitment.StatePayingDestination { - return errors.New("commitment in invalid state") - } - - commitmentRecord.State = commitment.StateReadyToOpen - return data.SaveCommitment(ctx, commitmentRecord) -} - -// Opening is something managed externally - func markCommitmentOpen(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) if err != nil { @@ -60,10 +40,13 @@ func markCommitmentOpen(ctx context.Context, data code_data.Provider, intentId s return nil } - if commitmentRecord.State != commitment.StateOpening { + if commitmentRecord.State != commitment.StatePayingDestination { return errors.New("commitment in invalid state") } + // todo: We lose out on some scheduling optimizations now that commitments + // are opened immediately. There's now active polling during the entire + // temporary privacy deadline window. fulfillmentRecords, err := data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, intentId, actionId) if err != nil { return err @@ -77,8 +60,6 @@ func markCommitmentOpen(ctx context.Context, data code_data.Provider, intentId s return data.SaveCommitment(ctx, commitmentRecord) } -// Closing is likely something managed externally - func markCommitmentClosed(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) if err != nil { diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index b0d0c25a..28974c60 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -4,7 +4,6 @@ import ( "context" "encoding/hex" "errors" - "math" "sync" "time" @@ -20,7 +19,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/treasury" transaction_util "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/solana" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/solana/token" ) @@ -114,7 +112,7 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) CanSubmitToBlockchai } return true, nil - case fulfillment.CloseDormantTimelockAccount, fulfillment.CloseEmptyTimelockAccount: + case fulfillment.CloseEmptyTimelockAccount: // Technically valid, but we won't open for these cases return false, nil default: @@ -129,6 +127,9 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) SupportsOnDemandTran } func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { + var vm *common.Account // todo: configure vm account + var memory *common.Account // todo: configure memory account + if fulfillmentRecord.FulfillmentType != fulfillment.InitializeLockedTimelockAccount { return nil, errors.New("invalid fulfillment type") } @@ -143,13 +144,20 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransact return nil, err } - // todo: a single function utility in the common package to do exactly how we're getting timelockAccounts - timelockAccounts, err := authorityAccount.GetTimelockAccounts(timelock_token.DataVersion1, common.KinMintAccount) + timelockAccounts, err := authorityAccount.GetTimelockAccounts(vm, common.KinMintAccount) if err != nil { return nil, err } - txn, err := transaction_util.MakeOpenAccountTransaction(selectedNonce.Account, selectedNonce.Blockhash, timelockAccounts) + txn, err := transaction_util.MakeOpenAccountTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + memory, + 0, // todo: reserve free space in the memory account + + timelockAccounts, + ) if err != nil { return nil, err } @@ -347,7 +355,7 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) OnSuccess(ctx context.Context, ful return err } - return onTokenAccountClosed(ctx, h.data, fulfillmentRecord, txnRecord) + return nil } func (h *NoPrivacyWithdrawFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { @@ -394,14 +402,14 @@ func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) CanSubmitToBlo return false, nil } - // The commitment vault must be opened before we can send funds to it + // The commitment must be opened before we can send funds to it if commitmentRecord.State != commitment.StateOpen { return false, nil } // Check the privacy upgrade deadline, which is one of many factors as to - // why we may have opened the commitment vault. We need to ensure the - // deadline is hit before proceeding. + // why we may have opened the commitment. We need to ensure the deadline + // is hit before proceeding. privacyUpgradeDeadline, err := commitment_worker.GetDeadlineToUpgradePrivacy(ctx, h.data, commitmentRecord) if err == commitment_worker.ErrNoPrivacyUpgradeDeadline { return false, nil @@ -523,12 +531,12 @@ func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) CanSubmitToBlo } // The old commitment record must be marked as diverting funds to the new - // intended commitment vault before proceeding. - if oldCommitmentRecord.RepaymentDivertedTo == nil || *oldCommitmentRecord.RepaymentDivertedTo != *fulfillmentRecord.Destination { + // intended commitment before proceeding. + if oldCommitmentRecord.RepaymentDivertedTo == nil { return false, nil } - newCommitmentRecord, err := h.data.GetCommitmentByVault(ctx, *fulfillmentRecord.Destination) + newCommitmentRecord, err := h.data.GetCommitmentByAddress(ctx, *oldCommitmentRecord.RepaymentDivertedTo) if err != nil { return false, err } @@ -686,6 +694,10 @@ func (h *TransferWithCommitmentFulfillmentHandler) SupportsOnDemandTransactions( } func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { + var vm *common.Account // todo: configure vm account + var accountMemory *common.Account // todo: configure memory account + var relayMemory *common.Account // todo: configure memory account + commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) if err != nil { return nil, err @@ -725,16 +737,22 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, err } - txn, err := transaction_util.MakeTreasuryAdvanceTransaction( + // todo: support external transfers + txn, err := transaction_util.MakeInternalTreasuryAdvanceTransaction( selectedNonce.Account, selectedNonce.Blockhash, + vm, + accountMemory, + 0, // todo: use indexer to find index + relayMemory, + 0, // todo: use indexer to find index + treasuryPool, treasuryPoolVault, destination, commitment, - commitmentRecord.PoolBump, - commitmentRecord.Amount, + uint32(commitmentRecord.Amount), // todo: assumes amount never overflows uint32 transcript, recentRoot, ) @@ -760,7 +778,7 @@ func (h *TransferWithCommitmentFulfillmentHandler) OnSuccess(ctx context.Context return err } - return markCommitmentReadyToOpen(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) + return markCommitmentOpen(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) } func (h *TransferWithCommitmentFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { @@ -823,11 +841,37 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) CanSubmitToBlockchain(ctx } func (h *CloseEmptyTimelockAccountFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false + return true } func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") + var vm *common.Account // todo: configure vm account + var memory *common.Account // todo: configure memory account + var storage *common.Account // todo: configure storage account + + if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { + return nil, errors.New("invalid fulfillment type") + } + + txn, err := transaction_util.MakeCompressAccountTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + vm, + memory, + 0, // todo: get account index + storage, + ) + if err != nil { + return nil, err + } + + err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) + if err != nil { + return nil, err + } + + return &txn, nil } func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { @@ -835,7 +879,7 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnSuccess(ctx context.Cont return errors.New("invalid fulfillment type") } - return onTokenAccountClosed(ctx, h.data, fulfillmentRecord, txnRecord) + return nil } func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { @@ -860,127 +904,6 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) IsRevoked(ctx context.Cont return false, false, nil } -type CloseDormantTimelockAccountFulfillmentHandler struct { - data code_data.Provider -} - -func NewCloseDormantTimelockAccountFulfillmentHandler(data code_data.Provider) FulfillmentHandler { - return &CloseDormantTimelockAccountFulfillmentHandler{ - data: data, - } -} - -func (h *CloseDormantTimelockAccountFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseDormantTimelockAccount { - return false, errors.New("invalid fulfillment type") - } - - // For now, we only ever save fulfillment records for gift cards, so the below - // check isn't necessary yet. However, if this is no longer the case, the code - // below should be uncommented, unless other flows warrant it. - /* - accountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, fulfillmentRecord.Source) - if err != nil { - return false, err - } - - // Sanity check that could avoid a distastrous scenario if we accidentally - // schedule something that's not a gift card - if accountInfoRecord.AccountType != commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - return false, errors.New("source must be a remote send gift card") - } - */ - - // The source account is a Code account, so we must validate it exists on - // the blockchain prior to sending funds from it. - isSourceAccountCreated, err := isTokenAccountOnBlockchain(ctx, h.data, fulfillmentRecord.Source) - if err != nil { - return false, err - } else if !isSourceAccountCreated { - return false, nil - } - - // The destination account might is a Code account, so we must validate it - // exists on the blockchain prior to send funds to it. - isDestinationAccountCreated, err := isTokenAccountOnBlockchain(ctx, h.data, *fulfillmentRecord.Destination) - if err != nil { - return false, err - } else if !isDestinationAccountCreated { - return false, nil - } - - // todo: We can have single "AsSourceOrDestination" query - - // The source account is a user account, so check that there are no other - // fulfillments where it's used as a source account before closing it. - earliestFulfillment, err := h.data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return false, err - } - if earliestFulfillment != nil && earliestFulfillment.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - // The source account is a user account, so check that there are no other - // fulfillments where it's used as a destination before closing it. - earliestFulfillment, err = h.data.GetFirstSchedulableFulfillmentByAddressAsDestination(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return false, err - } - if earliestFulfillment != nil && earliestFulfillment.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - return true, nil -} - -func (h *CloseDormantTimelockAccountFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false -} - -func (h *CloseDormantTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") -} - -func (h *CloseDormantTimelockAccountFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseDormantTimelockAccount { - return errors.New("invalid fulfillment type") - } - - return onTokenAccountClosed(ctx, h.data, fulfillmentRecord, txnRecord) -} - -func (h *CloseDormantTimelockAccountFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseDormantTimelockAccount { - return false, errors.New("invalid fulfillment type") - } - - return false, nil -} - -func (h *CloseDormantTimelockAccountFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseDormantTimelockAccount { - return false, false, errors.New("invalid fulfillment type") - } - - // Replace above logic with commented code if we decide to use CloseDormantAccount actions - timelockRecord, err := h.data.GetTimelockByVault(ctx, fulfillmentRecord.Source) - if err != nil { - return false, false, err - } - - if timelockRecord.IsClosed() { - err = markActionRevoked(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return false, false, err - } - - return true, false, nil - } - - return false, false, nil -} - type SaveRecentRootFulfillmentHandler struct { data code_data.Provider } @@ -1050,356 +973,71 @@ func (h *SaveRecentRootFulfillmentHandler) IsRevoked(ctx context.Context, fulfil return false, false, nil } -type InitializeCommitmentProofFulfillmentHandler struct { +type CloseCommitmentFulfillmentHandler struct { data code_data.Provider } -func NewInitializeCommitmentProofFulfillmentHandler(data code_data.Provider) FulfillmentHandler { - return &InitializeCommitmentProofFulfillmentHandler{ +func NewCloseCommitmentFulfillmentHandler(data code_data.Provider) FulfillmentHandler { + return &CloseCommitmentFulfillmentHandler{ data: data, } } -// Assumption: Commitment vault opening is pre-sorted at the very front of the line -func (h *InitializeCommitmentProofFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.InitializeCommitmentProof { - return false, errors.New("invalid fulfillment type") - } - - // Ensure the commitment record exists and it's in a valid state. - commitmentRecord, err := h.data.GetCommitmentByVault(ctx, fulfillmentRecord.Source) - if err != nil { - return false, err - } else if commitmentRecord.State != commitment.StateOpening { - return false, errors.New("commitment in unexpected state") - } - - // We're initializing a new proof for a commitment, so there are no dependencies - // as this is first one in the chain for opening commitment vaults. - return true, nil -} - -func (h *InitializeCommitmentProofFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false -} - -func (h *InitializeCommitmentProofFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") -} - -func (h *InitializeCommitmentProofFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.InitializeCommitmentProof { - return errors.New("invalid fulfillment type") - } - - fulfillmentRecord, err := h.data.GetNextSchedulableFulfillmentByAddress(ctx, fulfillmentRecord.Source, fulfillmentRecord.IntentOrderingIndex, fulfillmentRecord.ActionOrderingIndex, fulfillmentRecord.FulfillmentOrderingIndex) - if err != nil { - return err - } - return markFulfillmentAsActivelyScheduled(ctx, h.data, fulfillmentRecord) -} - -func (h *InitializeCommitmentProofFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.InitializeCommitmentProof { - return false, errors.New("invalid fulfillment type") - } - - // Let it fail. More than likely we have a bug. In theory, we could try implementing - // auto-recovery. - return false, nil -} - -func (h *InitializeCommitmentProofFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.InitializeCommitmentProof { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil -} - -type UploadCommitmentProofFulfillmentHandler struct { - data code_data.Provider -} - -func NewUploadCommitmentProofFulfillmentHandler(data code_data.Provider) FulfillmentHandler { - return &UploadCommitmentProofFulfillmentHandler{ - data: data, - } -} - -// Assumption: Commitment vault opening is pre-sorted at the very front of the line -func (h *UploadCommitmentProofFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.UploadCommitmentProof { - return false, errors.New("invalid fulfillment type") - } - - // Ensure the commitment record exists and it's in a valid state. - commitmentRecord, err := h.data.GetCommitmentByVault(ctx, fulfillmentRecord.Source) - if err != nil { - return false, err - } else if commitmentRecord.State != commitment.StateOpening { - return false, errors.New("commitment in unexpected state") - } - - // The source account is the commitment vault. Check that there isn't an earlier - // fulfillment in the process for opening the account. - earliestFulfillmentAsSource, err := h.data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return false, err - } - if earliestFulfillmentAsSource != nil && earliestFulfillmentAsSource.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - return true, nil -} - -func (h *UploadCommitmentProofFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false -} - -func (h *UploadCommitmentProofFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") -} - -func (h *UploadCommitmentProofFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.UploadCommitmentProof { - return errors.New("invalid fulfillment type") - } - - fulfillmentRecord, err := h.data.GetNextSchedulableFulfillmentByAddress(ctx, fulfillmentRecord.Source, fulfillmentRecord.IntentOrderingIndex, fulfillmentRecord.ActionOrderingIndex, fulfillmentRecord.FulfillmentOrderingIndex) - if err != nil { - return err - } - return markFulfillmentAsActivelyScheduled(ctx, h.data, fulfillmentRecord) -} - -func (h *UploadCommitmentProofFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.UploadCommitmentProof { - return false, errors.New("invalid fulfillment type") - } - - // Let it fail. More than likely we have a bug. In theory, we could try implementing - // auto-recovery. - return false, nil -} - -func (h *UploadCommitmentProofFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.UploadCommitmentProof { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil -} - -type VerifyCommitmentProofFulfillmentHandler struct { - data code_data.Provider -} - -func NewVerifyCommitmentProofFulfillmentHandler(data code_data.Provider) FulfillmentHandler { - return &VerifyCommitmentProofFulfillmentHandler{ - data: data, - } -} - -// Assumption: Commitment vault opening is pre-sorted at the very front of the line -func (h *VerifyCommitmentProofFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.VerifyCommitmentProof { - return false, errors.New("invalid fulfillment type") - } - - // Ensure the commitment record exists and it's in a valid state. - commitmentRecord, err := h.data.GetCommitmentByVault(ctx, fulfillmentRecord.Source) +// todo: New commitment closing flow not implemented yet +func (h *CloseCommitmentFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { + commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) if err != nil { return false, err - } else if commitmentRecord.State != commitment.StateOpening { - return false, errors.New("commitment in unexpected state") - } - - // The source account is the commitment vault. Check that there isn't an earlier - // fulfillment in the process for opening the account. - earliestFulfillmentAsSource, err := h.data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return false, err - } - if earliestFulfillmentAsSource != nil && earliestFulfillmentAsSource.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - return true, nil -} - -func (h *VerifyCommitmentProofFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false -} - -func (h *VerifyCommitmentProofFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") -} - -func (h *VerifyCommitmentProofFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.VerifyCommitmentProof { - return errors.New("invalid fulfillment type") } - fulfillmentRecord, err := h.data.GetNextSchedulableFulfillmentByAddress(ctx, fulfillmentRecord.Source, fulfillmentRecord.IntentOrderingIndex, fulfillmentRecord.ActionOrderingIndex, fulfillmentRecord.FulfillmentOrderingIndex) - if err != nil { - return err - } - return markFulfillmentAsActivelyScheduled(ctx, h.data, fulfillmentRecord) + // Commitment worker guarantees all cheques have been cashed + return commitmentRecord.State == commitment.StateClosing, nil } -func (h *VerifyCommitmentProofFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.VerifyCommitmentProof { - return false, errors.New("invalid fulfillment type") - } - - // Let it fail. More than likely we have a bug. In theory, we could try implementing - // auto-recovery. - return false, nil -} - -func (h *VerifyCommitmentProofFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.VerifyCommitmentProof { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil +func (h *CloseCommitmentFulfillmentHandler) SupportsOnDemandTransactions() bool { + return true } -type OpenCommitmentVaultFulfillmentHandler struct { - data code_data.Provider -} +func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { + var vm *common.Account // todo: configure vm account + var memory *common.Account // todo: configure memory account + var storage *common.Account // todo: configure storage account -func NewOpenCommitmentVaultFulfillmentHandler(data code_data.Provider) FulfillmentHandler { - return &OpenCommitmentVaultFulfillmentHandler{ - data: data, + if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { + return nil, errors.New("invalid fulfillment type") } -} -// Assumption: Commitment vault opening is pre-sorted at the very front of the line -func (h *OpenCommitmentVaultFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.OpenCommitmentVault { - return false, errors.New("invalid fulfillment type") - } + txn, err := transaction_util.MakeCompressAccountTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, - // Ensure the commitment record exists and it's in a valid state. - commitmentRecord, err := h.data.GetCommitmentByVault(ctx, fulfillmentRecord.Source) + vm, + memory, + 0, // todo: get account index + storage, + ) if err != nil { - return false, err - } else if commitmentRecord.State != commitment.StateOpening { - return false, errors.New("commitment in unexpected state") - } - - // The source account is the commitment vault. Check that there isn't an earlier - // fulfillment in the process for opening the account. - earliestFulfillmentAsSource, err := h.data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return false, err - } - if earliestFulfillmentAsSource != nil && earliestFulfillmentAsSource.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - return true, nil -} - -func (h *OpenCommitmentVaultFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false -} - -func (h *OpenCommitmentVaultFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") -} - -func (h *OpenCommitmentVaultFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.OpenCommitmentVault { - return errors.New("invalid fulfillment type") - } - - return markCommitmentOpen(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) -} - -func (h *OpenCommitmentVaultFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.OpenCommitmentVault { - return false, errors.New("invalid fulfillment type") - } - - // Let it fail. More than likely we have a bug. In theory, we could try implementing - // auto-recovery. - return false, nil -} - -func (h *OpenCommitmentVaultFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.OpenCommitmentVault { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil -} - -type CloseCommitmentVaultFulfillmentHandler struct { - data code_data.Provider -} - -func NewCloseCommitmentVaultFulfillmentHandler(data code_data.Provider) FulfillmentHandler { - return &CloseCommitmentVaultFulfillmentHandler{ - data: data, - } -} - -// Assumption: Commitment vault closing is pre-sorted to the very last position in the line -func (h *CloseCommitmentVaultFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitmentVault { - return false, errors.New("invalid fulfillment type") + return nil, err } - commitmentRecord, err := h.data.GetCommitmentByVault(ctx, fulfillmentRecord.Source) + err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) if err != nil { - return false, err - } else if commitmentRecord.State != commitment.StateClosing { - return false, nil - } - - var scheduableCount uint64 - for _, scheduableState := range []fulfillment.State{ - fulfillment.StateUnknown, - fulfillment.StatePending, - } { - count, err := h.data.GetFulfillmentCountByStateAndAddress(ctx, scheduableState, fulfillmentRecord.Source) - if err != nil { - return false, err - } - - scheduableCount += count + return nil, err } - // In the end, we should end up with one scheduleable fulfillment, and that's - // the fulfillment we're operating on right now. If the commitment is in the - // closing state, then the worker must have checked that all possible payments - // (including any potential new future upgrades) to this commitment vault have - // been played out. This is simply a sanity check of that assumption. - return scheduableCount == 1, nil -} - -func (h *CloseCommitmentVaultFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false -} - -func (h *CloseCommitmentVaultFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") + return &txn, nil } -func (h *CloseCommitmentVaultFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitmentVault { +func (h *CloseCommitmentFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { + if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { return errors.New("invalid fulfillment type") } return markCommitmentClosed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) } -func (h *CloseCommitmentVaultFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitmentVault { +func (h *CloseCommitmentFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { + if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { return false, errors.New("invalid fulfillment type") } @@ -1408,8 +1046,8 @@ func (h *CloseCommitmentVaultFulfillmentHandler) OnFailure(ctx context.Context, return false, nil } -func (h *CloseCommitmentVaultFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitmentVault { +func (h *CloseCommitmentFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { + if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { return false, false, errors.New("invalid fulfillment type") } @@ -1460,30 +1098,6 @@ func isTokenAccountOnBlockchain(ctx context.Context, data code_data.Provider, ad return existsOnBlockchain, nil } -func onTokenAccountClosed(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - var closedAccount string - switch fulfillmentRecord.FulfillmentType { - case fulfillment.CloseEmptyTimelockAccount, fulfillment.NoPrivacyWithdraw, fulfillment.CloseDormantTimelockAccount: - closedAccount = fulfillmentRecord.Source - default: - return errors.New("unhanlded fulfillment type") - } - - if fulfillmentRecord.FulfillmentType != fulfillment.CloseDormantTimelockAccount { - closeDormantFulfillmentRecord, err := data.GetNextSchedulableFulfillmentByAddress(ctx, closedAccount, uint64(math.MaxInt64)-1, 0, 0) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return err - } else if err == nil { - err = markFulfillmentAsActivelyScheduled(ctx, data, closeDormantFulfillmentRecord) - if err != nil { - return err - } - } - } - - return markTimelockClosed(ctx, data, closedAccount, txnRecord.Slot) -} - func estimateTreasuryPoolFundingLevels(ctx context.Context, data code_data.Provider, record *treasury.Record) (total uint64, used uint64, err error) { total, err = data.GetTotalAvailableTreasuryPoolFunds(ctx, record.Vault) if err != nil { @@ -1507,12 +1121,7 @@ func getFulfillmentHandlers(data code_data.Provider, configProvider ConfigProvid handlersByType[fulfillment.PermanentPrivacyTransferWithAuthority] = NewPermanentPrivacyTransferWithAuthorityFulfillmentHandler(data, configProvider) handlersByType[fulfillment.TransferWithCommitment] = NewTransferWithCommitmentFulfillmentHandler(data) handlersByType[fulfillment.CloseEmptyTimelockAccount] = NewCloseEmptyTimelockAccountFulfillmentHandler(data) - handlersByType[fulfillment.CloseDormantTimelockAccount] = NewCloseDormantTimelockAccountFulfillmentHandler(data) handlersByType[fulfillment.SaveRecentRoot] = NewSaveRecentRootFulfillmentHandler(data) - handlersByType[fulfillment.InitializeCommitmentProof] = NewInitializeCommitmentProofFulfillmentHandler(data) - handlersByType[fulfillment.UploadCommitmentProof] = NewUploadCommitmentProofFulfillmentHandler(data) - handlersByType[fulfillment.VerifyCommitmentProof] = NewVerifyCommitmentProofFulfillmentHandler(data) - handlersByType[fulfillment.OpenCommitmentVault] = NewOpenCommitmentVaultFulfillmentHandler(data) - handlersByType[fulfillment.CloseCommitmentVault] = NewCloseCommitmentVaultFulfillmentHandler(data) + handlersByType[fulfillment.CloseCommitment] = NewCloseCommitmentFulfillmentHandler(data) return handlersByType } diff --git a/pkg/code/async/sequencer/fulfillment_handler_test.go b/pkg/code/async/sequencer/fulfillment_handler_test.go index 73ceade3..7e5d6dcd 100644 --- a/pkg/code/async/sequencer/fulfillment_handler_test.go +++ b/pkg/code/async/sequencer/fulfillment_handler_test.go @@ -1,42 +1,8 @@ package async_sequencer -import ( - "context" - "crypto/ed25519" - "crypto/sha256" - "encoding/hex" - "fmt" - "math/rand" - "strings" - "testing" - "time" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/currency" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/nonce" - "github.com/code-payments/code-server/pkg/code/data/timelock" - "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/code/data/vault" - transaction_util "github.com/code-payments/code-server/pkg/code/transaction" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/memo" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/solana/system" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" - "github.com/code-payments/code-server/pkg/testutil" -) +// todo: fix tests once sequencer is rounded out for the vm + +/* // Note: CanSubmitToBlockchain tests are handled in scheduler testing @@ -1273,3 +1239,5 @@ func assertExpectedKreMemoInstruction(t *testing.T, txn *solana.Transaction, ind assert.EqualValues(t, kin.TransactionTypeP2P, kreMemo.TransactionType()) assert.EqualValues(t, transaction_util.KreAppIndex, kreMemo.AppIndex()) } + +*/ diff --git a/pkg/code/async/sequencer/intent_handler_test.go b/pkg/code/async/sequencer/intent_handler_test.go index 00ada0ca..71329785 100644 --- a/pkg/code/async/sequencer/intent_handler_test.go +++ b/pkg/code/async/sequencer/intent_handler_test.go @@ -1,23 +1,8 @@ package async_sequencer -import ( - "context" - "fmt" - "math" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/testutil" -) +// todo: fix tests once sequencer is rounded out for the vm + +/* func TestOpenAccountsIntentHandler_RemainInStatePending(t *testing.T) { env := setupIntentHandlerTestEnv(t) @@ -1013,3 +998,5 @@ func (e *intentHandlerTestEnv) assertSchedulerPollingState(t *testing.T, intentI assert.Equal(t, expected, !fulfillmentRecord.DisableActiveScheduling) } } + +*/ diff --git a/pkg/code/async/sequencer/scheduler_test.go b/pkg/code/async/sequencer/scheduler_test.go index 6c2dd1aa..36820d8f 100644 --- a/pkg/code/async/sequencer/scheduler_test.go +++ b/pkg/code/async/sequencer/scheduler_test.go @@ -1,40 +1,8 @@ package async_sequencer -import ( - "context" - "fmt" - "math" - "math/rand" - "os" - "sort" - "strconv" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/currency" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/timelock" - "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/code-payments/code-server/pkg/code/data/treasury" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" - "github.com/code-payments/code-server/pkg/testutil" -) +// todo: fix tests once sequencer is rounded out for the vm + +/* // todo: Still not entirely happy how temporary incoming/outgoing accounts are handled in these tests. Lots of manual and error prone input still. @@ -3144,3 +3112,5 @@ func printForTest(msg string, args ...any) { fmt.Printf(msg, args...) } + +*/ diff --git a/pkg/code/async/sequencer/timelock.go b/pkg/code/async/sequencer/timelock.go index 9fd43589..46a3ae19 100644 --- a/pkg/code/async/sequencer/timelock.go +++ b/pkg/code/async/sequencer/timelock.go @@ -3,9 +3,9 @@ package async_sequencer import ( "context" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/timelock" + timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) // The faster we can update timelock state, the better it is to unblock scheduling. @@ -28,19 +28,3 @@ func markTimelockLocked(ctx context.Context, data code_data.Provider, vault stri } return err } - -func markTimelockClosed(ctx context.Context, data code_data.Provider, vault string, slot uint64) error { - record, err := data.GetTimelockByVault(ctx, vault) - if err != nil { - return err - } - - record.VaultState = timelock_token_v1.StateClosed - record.Block = slot - - err = data.SaveTimelock(ctx, record) - if err == timelock.ErrStaleTimelockState { - return nil - } - return err -} diff --git a/pkg/code/async/sequencer/utils.go b/pkg/code/async/sequencer/utils.go index 3c1405f7..821ec816 100644 --- a/pkg/code/async/sequencer/utils.go +++ b/pkg/code/async/sequencer/utils.go @@ -5,11 +5,11 @@ import ( "github.com/pkg/errors" - "github.com/code-payments/code-server/pkg/solana" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/transaction" + "github.com/code-payments/code-server/pkg/solana" ) func (p *service) validateFulfillmentState(record *fulfillment.Record, states ...fulfillment.State) error { @@ -42,6 +42,11 @@ func (p *service) markFulfillmentConfirmed(ctx context.Context, record *fulfillm return err } + err = p.markVirtualNonceReleasedDueToSubmittedTransaction(ctx, record) + if err != nil { + return err + } + record.State = fulfillment.StateConfirmed record.Data = nil return p.data.UpdateFulfillment(ctx, record) @@ -58,6 +63,11 @@ func (p *service) markFulfillmentFailed(ctx context.Context, record *fulfillment return err } + err = p.markVirtualNonceReleasedDueToSubmittedTransaction(ctx, record) + if err != nil { + return err + } + record.State = fulfillment.StateFailed record.Data = nil return p.data.UpdateFulfillment(ctx, record) @@ -81,6 +91,11 @@ func (p *service) markFulfillmentRevoked(ctx context.Context, fulfillmentRecord if err != nil { return err } + + err = p.markVirtualNonceAvailableDueToRevokedFulfillment(ctx, fulfillmentRecord) + if err != nil { + return err + } } fulfillmentRecord.State = fulfillment.StateRevoked @@ -213,3 +228,71 @@ func (p *service) markNonceReleasedDueToSubmittedTransaction(ctx context.Context nonceRecord.State = nonce.StateReleased return p.data.SaveNonce(ctx, nonceRecord) } + +// Important Note: Do NOT call this if the fulfillment being revoked is due to +// transactions having shared nonce blockhashes! +func (p *service) markVirtualNonceAvailableDueToRevokedFulfillment(ctx context.Context, fulfillmentToRevoke *fulfillment.Record) error { + // We'll only automatically manage the nonce state if the fulfillment is in + // an unknown state. Otherwise, there's a chance it was submitted and could + // have been used. A human is needed to resolve it. + if fulfillmentToRevoke.State != fulfillment.StateUnknown { + return errors.New("fulfillment is in dangerous state to manage nonce state") + } + + // Transaction doesn't have an assigned virtual nonce + if fulfillmentToRevoke.VirtualNonce == nil { + return nil + } + + nonceRecord, err := p.data.GetNonce(ctx, *fulfillmentToRevoke.VirtualNonce) + if err != nil { + return err + } + + if *fulfillmentToRevoke.VirtualSignature != nonceRecord.Signature { + return errors.New("unexpected virtual nonce signature") + } + + if *fulfillmentToRevoke.VirtualBlockhash != nonceRecord.Blockhash { + return errors.New("unexpected virtual nonce blockhash") + } + + if nonceRecord.State != nonce.StateReserved { + return errors.New("unexpected virtual nonce state") + } + + nonceRecord.State = nonce.StateAvailable + nonceRecord.Signature = "" + return p.data.SaveNonce(ctx, nonceRecord) +} + +func (p *service) markVirtualNonceReleasedDueToSubmittedTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record) error { + if fulfillmentRecord.State != fulfillment.StatePending { + return errors.New("fulfillment is in unexpected state") + } + + // Transaction doesn't have an assigned virtual nonce + if fulfillmentRecord.VirtualNonce == nil { + return nil + } + + nonceRecord, err := p.data.GetNonce(ctx, *fulfillmentRecord.VirtualNonce) + if err != nil { + return err + } + + if *fulfillmentRecord.VirtualSignature != nonceRecord.Signature { + return errors.New("unexpected virtual nonce signature") + } + + if *fulfillmentRecord.VirtualBlockhash != nonceRecord.Blockhash { + return errors.New("unexpected virtual nonce blockhash") + } + + if nonceRecord.State != nonce.StateReserved { + return errors.New("unexpected virtual nonce state") + } + + nonceRecord.State = nonce.StateReleased + return p.data.SaveNonce(ctx, nonceRecord) +} diff --git a/pkg/code/async/sequencer/utils_test.go b/pkg/code/async/sequencer/utils_test.go index 7b64ec07..56a91588 100644 --- a/pkg/code/async/sequencer/utils_test.go +++ b/pkg/code/async/sequencer/utils_test.go @@ -11,6 +11,8 @@ import ( "github.com/code-payments/code-server/pkg/code/data/nonce" ) +// todo: add virtual nonce test variants + func TestMarkNonceAsAvailableDueToRevokedFulfillment_SafetyChecks(t *testing.T) { env := setupWorkerEnv(t) diff --git a/pkg/code/async/sequencer/worker_test.go b/pkg/code/async/sequencer/worker_test.go index e2b4823f..d1ef0df0 100644 --- a/pkg/code/async/sequencer/worker_test.go +++ b/pkg/code/async/sequencer/worker_test.go @@ -26,6 +26,8 @@ import ( "github.com/code-payments/code-server/pkg/testutil" ) +// todo: include new virtual nonce account handling tests + func TestFulfillmentWorker_StateUnknown_RemainInStateUnknown(t *testing.T) { env := setupWorkerEnv(t) diff --git a/pkg/code/balance/calculator_test.go b/pkg/code/balance/calculator_test.go index 1e4ec1e0..6251f93d 100644 --- a/pkg/code/balance/calculator_test.go +++ b/pkg/code/balance/calculator_test.go @@ -29,11 +29,13 @@ import ( func TestDefaultCalculationMethods_NewCodeAccount(t *testing.T) { env := setupBalanceTestEnv(t) + vmAccount := testutil.NewRandomAccount(t) newOwnerAccount := testutil.NewRandomAccount(t) - newTokenAccount, err := newOwnerAccount.ToTimelockVault(common.KinMintAccount) + newTokenAccount, err := newOwnerAccount.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) data := &balanceTestData{ + vmAccount: vmAccount, codeUsers: []*common.Account{newOwnerAccount}, } @@ -60,13 +62,15 @@ func TestDefaultCalculationMethods_NewCodeAccount(t *testing.T) { func TestDefaultCalculationMethods_DepositFromExternalWallet(t *testing.T) { env := setupBalanceTestEnv(t) + vmAccount := testutil.NewRandomAccount(t) owner := testutil.NewRandomAccount(t) - depositAccount, err := owner.ToTimelockVault(common.KinMintAccount) + depositAccount, err := owner.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) data := &balanceTestData{ + vmAccount: vmAccount, codeUsers: []*common.Account{owner}, transactions: []balanceTestTransaction{ // The following entries are added to the balance @@ -101,25 +105,28 @@ func TestDefaultCalculationMethods_DepositFromExternalWallet(t *testing.T) { func TestDefaultCalculationMethods_MultipleIntents(t *testing.T) { env := setupBalanceTestEnv(t) + vmAccount := testutil.NewRandomAccount(t) + owner1 := testutil.NewRandomAccount(t) - a1, err := owner1.ToTimelockVault(common.KinMintAccount) + a1, err := owner1.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) owner2 := testutil.NewRandomAccount(t) - a2, err := owner2.ToTimelockVault(common.KinMintAccount) + a2, err := owner2.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) owner3 := testutil.NewRandomAccount(t) - a3, err := owner3.ToTimelockVault(common.KinMintAccount) + a3, err := owner3.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) owner4 := testutil.NewRandomAccount(t) - a4, err := owner4.ToTimelockVault(common.KinMintAccount) + a4, err := owner4.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) data := &balanceTestData{ + vmAccount: vmAccount, codeUsers: []*common.Account{owner1, owner2, owner3, owner4}, transactions: []balanceTestTransaction{ // Fund account a1 through a4 with an external deposit @@ -198,17 +205,20 @@ func TestDefaultCalculationMethods_MultipleIntents(t *testing.T) { func TestDefaultCalculationMethods_BackAndForth(t *testing.T) { env := setupBalanceTestEnv(t) + vmAccount := testutil.NewRandomAccount(t) + owner1 := testutil.NewRandomAccount(t) - a1, err := owner1.ToTimelockVault(common.KinMintAccount) + a1, err := owner1.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) owner2 := testutil.NewRandomAccount(t) - a2, err := owner2.ToTimelockVault(common.KinMintAccount) + a2, err := owner2.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) data := &balanceTestData{ + vmAccount: vmAccount, codeUsers: []*common.Account{owner1, owner2}, transactions: []balanceTestTransaction{ // Fund account a1 through an external deposit @@ -254,13 +264,15 @@ func TestDefaultCalculationMethods_BackAndForth(t *testing.T) { func TestDefaultCalculationMethods_SelfPayments(t *testing.T) { env := setupBalanceTestEnv(t) + vmAccount := testutil.NewRandomAccount(t) ownerAccount := testutil.NewRandomAccount(t) - tokenAccount, err := ownerAccount.ToTimelockVault(common.KinMintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) data := &balanceTestData{ + vmAccount: vmAccount, codeUsers: []*common.Account{ownerAccount}, transactions: []balanceTestTransaction{ // Fund account the token account through an external deposit @@ -297,11 +309,13 @@ func TestDefaultCalculationMethods_SelfPayments(t *testing.T) { func TestDefaultCalculationMethods_NotManagedByCode(t *testing.T) { env := setupBalanceTestEnv(t) + vmAccount := testutil.NewRandomAccount(t) ownerAccount := testutil.NewRandomAccount(t) - tokenAccount, err := ownerAccount.ToTimelockVault(common.KinMintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, common.KinMintAccount) require.NoError(t, err) data := &balanceTestData{ + vmAccount: vmAccount, codeUsers: []*common.Account{ownerAccount}, } @@ -338,6 +352,7 @@ func TestDefaultCalculation_ExternalAccount(t *testing.T) { func TestGetAggregatedBalances(t *testing.T) { env := setupBalanceTestEnv(t) + vmAccount := testutil.NewRandomAccount(t) owner := testutil.NewRandomAccount(t) _, err := GetPrivateBalance(env.ctx, env.data, owner) @@ -360,7 +375,7 @@ func TestGetAggregatedBalances(t *testing.T) { expectedPrivateBalance += balance } - timelockAccounts, err := authority.GetTimelockAccounts(common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(vmAccount, common.KinMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -400,6 +415,7 @@ type balanceTestEnv struct { } type balanceTestData struct { + vmAccount *common.Account codeUsers []*common.Account transactions []balanceTestTransaction } @@ -424,7 +440,7 @@ func setupBalanceTestEnv(t *testing.T) (env balanceTestEnv) { func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestData) { for _, owner := range data.codeUsers { - timelockAccounts, err := owner.GetTimelockAccounts(common.KinMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(data.vmAccount, common.KinMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() timelockRecord.VaultState = timelock_token_v1.StateLocked diff --git a/pkg/code/common/account.go b/pkg/code/common/account.go index 46903ee1..99bf6acb 100644 --- a/pkg/code/common/account.go +++ b/pkg/code/common/account.go @@ -14,6 +14,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/solana/token" ) @@ -28,12 +29,17 @@ type Account struct { } type TimelockAccounts struct { + Vm *Account + State *Account StateBump uint8 Vault *Account VaultBump uint8 + Unlock *Account + UnlockBump uint8 + VaultOwner *Account Mint *Account @@ -155,12 +161,12 @@ func (a *Account) Sign(message []byte) ([]byte, error) { return signature, nil } -func (a *Account) ToTimelockVault(mint *Account) (*Account, error) { +func (a *Account) ToTimelockVault(vm, mint *Account) (*Account, error) { if err := a.Validate(); err != nil { return nil, errors.Wrap(err, "error validating owner account") } - timelockAccounts, err := a.GetTimelockAccounts(mint) + timelockAccounts, err := a.GetTimelockAccounts(vm, mint) if err != nil { return nil, err } @@ -180,29 +186,37 @@ func (a *Account) ToAssociatedTokenAccount(mint *Account) (*Account, error) { return NewAccountFromPublicKeyBytes(ata) } -func (a *Account) GetTimelockAccounts(mint *Account) (*TimelockAccounts, error) { +func (a *Account) GetTimelockAccounts(vm, mint *Account) (*TimelockAccounts, error) { if err := a.Validate(); err != nil { return nil, errors.Wrap(err, "error validating owner account") } - stateAddress, stateBump, err := timelock_token_v1.GetStateAddress(&timelock_token_v1.GetStateAddressArgs{ - Mint: mint.publicKey.ToBytes(), - TimeAuthority: GetSubsidizer().publicKey.ToBytes(), - VaultOwner: a.publicKey.ToBytes(), - NumDaysLocked: timelock_token_v1.DefaultNumDaysLocked, + stateAddress, stateBump, err := cvm.GetVirtualTimelockAccountAddress(&cvm.GetVirtualTimelockAccountAddressArgs{ + Mint: mint.publicKey.ToBytes(), + VmAuthority: GetSubsidizer().publicKey.ToBytes(), + Owner: a.publicKey.ToBytes(), + LockDuration: timelock_token_v1.DefaultNumDaysLocked, }) if err != nil { return nil, errors.Wrap(err, "error getting timelock state address") } - vaultAddress, vaultBump, err := timelock_token_v1.GetVaultAddress(&timelock_token_v1.GetVaultAddressArgs{ - State: stateAddress, - DataVersion: timelock_token_v1.DataVersion1, + vaultAddress, vaultBump, err := cvm.GetVirtualTimelockVaultAddress(&cvm.GetVirtualTimelockVaultAddressArgs{ + VirtualTimelock: stateAddress, }) if err != nil { return nil, errors.Wrap(err, "error getting vault address") } + unlockAddress, unlockBump, err := cvm.GetVmUnlockStateAccountAddress(&cvm.GetVmUnlockStateAccountAddressArgs{ + Owner: a.publicKey.ToBytes(), + VirtualTimelock: stateAddress, + Vm: vm.publicKey.ToBytes(), + }) + if err != nil { + return nil, errors.Wrap(err, "error getting unlock address") + } + stateAccount, err := NewAccountFromPublicKeyBytes(stateAddress) if err != nil { return nil, errors.Wrap(err, "invalid state address") @@ -213,7 +227,14 @@ func (a *Account) GetTimelockAccounts(mint *Account) (*TimelockAccounts, error) return nil, errors.Wrap(err, "invalid vault address") } + unlockAccount, err := NewAccountFromPublicKeyBytes(unlockAddress) + if err != nil { + return nil, errors.Wrap(err, "invalid unlock address") + } + return &TimelockAccounts{ + Vm: vm, + VaultOwner: a, State: stateAccount, @@ -222,6 +243,9 @@ func (a *Account) GetTimelockAccounts(mint *Account) (*TimelockAccounts, error) Vault: vaultAccount, VaultBump: vaultBump, + Unlock: unlockAccount, + UnlockBump: unlockBump, + Mint: mint, }, nil } @@ -318,6 +342,24 @@ func (a *TimelockAccounts) GetDBRecord(ctx context.Context, data code_data.Provi return data.GetTimelockByVault(ctx, a.Vault.publicKey.ToBase58()) } +// GetInitializeInstruction gets a SystemTimelockInitInstruction instruction for a timelock account +func (a *TimelockAccounts) GetInitializeInstruction(memory *Account, accountIndex uint16) (solana.Instruction, error) { + return cvm.NewSystemTimelockInitInstruction( + &cvm.SystemTimelockInitInstructionAccounts{ + VmAuthority: GetSubsidizer().publicKey.ToBytes(), + Vm: a.Vm.PublicKey().ToBytes(), + VmMemory: memory.PublicKey().ToBytes(), + VirtualAccountOwner: a.VaultOwner.PublicKey().ToBytes(), + }, + &cvm.SystemTimelockInitInstructionArgs{ + AccountIndex: accountIndex, + VirtualTimelockBump: a.StateBump, + VirtualVaultBump: a.VaultBump, + VmUnlockPdaBump: a.UnlockBump, + }, + ), nil +} + // GetTransferWithAuthorityInstruction gets a TransferWithAuthority instruction for a timelock account func (a *TimelockAccounts) GetTransferWithAuthorityInstruction(destination *Account, quarks uint64) (solana.Instruction, error) { if err := destination.Validate(); err != nil { diff --git a/pkg/code/common/account_test.go b/pkg/code/common/account_test.go index b66a7ac0..f78c3906 100644 --- a/pkg/code/common/account_test.go +++ b/pkg/code/common/account_test.go @@ -14,6 +14,7 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/solana/token" ) @@ -104,54 +105,64 @@ func TestInvalidAccount(t *testing.T) { } func TestConvertToTimelockVault(t *testing.T) { + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - stateAddress, _, err := timelock_token_v1.GetStateAddress(&timelock_token_v1.GetStateAddressArgs{ - Mint: mintAccount.PublicKey().ToBytes(), - TimeAuthority: subsidizerAccount.PublicKey().ToBytes(), - VaultOwner: ownerAccount.PublicKey().ToBytes(), - NumDaysLocked: timelock_token_v1.DefaultNumDaysLocked, + stateAddress, _, err := cvm.GetVirtualTimelockAccountAddress(&cvm.GetVirtualTimelockAccountAddressArgs{ + Mint: mintAccount.PublicKey().ToBytes(), + VmAuthority: subsidizerAccount.PublicKey().ToBytes(), + Owner: ownerAccount.PublicKey().ToBytes(), + LockDuration: timelock_token_v1.DefaultNumDaysLocked, }) require.NoError(t, err) - expectedVaultAddress, _, err := timelock_token_v1.GetVaultAddress(&timelock_token_v1.GetVaultAddressArgs{ - State: stateAddress, - DataVersion: timelock_token_v1.DataVersion1, + expectedVaultAddress, _, err := cvm.GetVirtualTimelockVaultAddress(&cvm.GetVirtualTimelockVaultAddressArgs{ + VirtualTimelock: stateAddress, }) require.NoError(t, err) - tokenAccount, err := ownerAccount.ToTimelockVault(mintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, mintAccount) require.NoError(t, err) assert.EqualValues(t, expectedVaultAddress, tokenAccount.PublicKey().ToBytes()) } func TestGetTimelockAccounts(t *testing.T) { + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - expectedStateAddress, expectedStateBump, err := timelock_token_v1.GetStateAddress(&timelock_token_v1.GetStateAddressArgs{ - Mint: mintAccount.PublicKey().ToBytes(), - TimeAuthority: subsidizerAccount.PublicKey().ToBytes(), - VaultOwner: ownerAccount.PublicKey().ToBytes(), - NumDaysLocked: timelock_token_v1.DefaultNumDaysLocked, + expectedStateAddress, expectedStateBump, err := cvm.GetVirtualTimelockAccountAddress(&cvm.GetVirtualTimelockAccountAddressArgs{ + Mint: mintAccount.PublicKey().ToBytes(), + VmAuthority: subsidizerAccount.PublicKey().ToBytes(), + Owner: ownerAccount.PublicKey().ToBytes(), + LockDuration: timelock_token_v1.DefaultNumDaysLocked, }) require.NoError(t, err) - expectedVaultAddress, expectedVaultBump, err := timelock_token_v1.GetVaultAddress(&timelock_token_v1.GetVaultAddressArgs{ - State: expectedStateAddress, - DataVersion: timelock_token_v1.DataVersion1, + expectedVaultAddress, expectedVaultBump, err := cvm.GetVirtualTimelockVaultAddress(&cvm.GetVirtualTimelockVaultAddressArgs{ + VirtualTimelock: expectedStateAddress, }) require.NoError(t, err) - actual, err := ownerAccount.GetTimelockAccounts(mintAccount) + expectedUnlockAddress, expectedUnlockBump, err := cvm.GetVmUnlockStateAccountAddress(&cvm.GetVmUnlockStateAccountAddressArgs{ + Owner: ownerAccount.PublicKey().ToBytes(), + VirtualTimelock: expectedStateAddress, + Vm: vmAccount.PublicKey().ToBytes(), + }) + require.NoError(t, err) + + actual, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) + assert.EqualValues(t, vmAccount.PublicKey().ToBytes(), actual.Vm.PublicKey().ToBytes()) assert.EqualValues(t, expectedStateAddress, actual.State.PublicKey().ToBytes()) assert.Equal(t, expectedStateBump, actual.StateBump) assert.EqualValues(t, expectedVaultAddress, actual.Vault.PublicKey().ToBytes()) assert.Equal(t, expectedVaultBump, actual.VaultBump) + assert.EqualValues(t, expectedUnlockAddress, actual.Unlock.PublicKey().ToBytes()) + assert.Equal(t, expectedUnlockBump, actual.UnlockBump) assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), actual.VaultOwner.PublicKey().ToBytes()) assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), actual.Mint.PublicKey().ToBytes()) } @@ -160,10 +171,11 @@ func TestIsAccountManagedByCode_TimelockState(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() + vmAccount := newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) // No record of the account anywhere @@ -210,10 +222,11 @@ func TestIsAccountManagedByCode_OtherAccounts(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() + vmAccount := newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) require.NoError(t, data.SaveTimelock(ctx, timelockAccounts.ToDBRecord())) @@ -230,12 +243,17 @@ func TestIsAccountManagedByCode_OtherAccounts(t *testing.T) { assert.False(t, result) } +func TestGetInitializeInstruction(t *testing.T) { + // todo: implement me +} + func TestGetTransferWithAuthorityInstruction(t *testing.T) { + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - source, err := ownerAccount.GetTimelockAccounts(mintAccount) + source, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) destination := newRandomTestAccount(t) @@ -261,11 +279,12 @@ func TestGetTransferWithAuthorityInstruction(t *testing.T) { } func TestGetWithdrawInstruction(t *testing.T) { + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - source, err := ownerAccount.GetTimelockAccounts(mintAccount) + source, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) destination := newRandomTestAccount(t) @@ -288,11 +307,12 @@ func TestGetWithdrawInstruction(t *testing.T) { } func TestGetBurnDustWithAuthorityInstruction(t *testing.T) { + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) maxAmount := kin.ToQuarks(1) @@ -317,11 +337,12 @@ func TestGetBurnDustWithAuthorityInstruction(t *testing.T) { } func TestGetRevokeLockWithAuthorityInstruction(t *testing.T) { + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) ixn, err := timelockAccounts.GetRevokeLockWithAuthorityInstruction() @@ -341,11 +362,12 @@ func TestGetRevokeLockWithAuthorityInstruction(t *testing.T) { } func TestGetDeactivateInstruction(t *testing.T) { + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) ixn, err := timelockAccounts.GetDeactivateInstruction() @@ -364,11 +386,12 @@ func TestGetDeactivateInstruction(t *testing.T) { } func TestGetCloseAccountsInstruction(t *testing.T) { + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) ixn, err := timelockAccounts.GetCloseAccountsInstruction() diff --git a/pkg/code/common/owner_test.go b/pkg/code/common/owner_test.go index 50f89c21..be842acc 100644 --- a/pkg/code/common/owner_test.go +++ b/pkg/code/common/owner_test.go @@ -20,6 +20,7 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) owner := newRandomTestAccount(t) swapAuthority := newRandomTestAccount(t) @@ -51,7 +52,7 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { // Later calls intent to OpenAccounts - timelockAccounts, err := owner.GetTimelockAccounts(coreMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(vmAccount, coreMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -114,6 +115,7 @@ func TestGetOwnerMetadata_RemoteSendGiftCard(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) owner := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) @@ -131,7 +133,7 @@ func TestGetOwnerMetadata_RemoteSendGiftCard(t *testing.T) { } require.NoError(t, data.SavePhoneVerification(ctx, verificationRecord)) - timelockAccounts, err := owner.GetTimelockAccounts(mintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -158,6 +160,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() + vmAccount := newRandomTestAccount(t) subsidizerAccount = newRandomTestAccount(t) owner := newRandomTestAccount(t) coreMintAccount := newRandomTestAccount(t) @@ -180,7 +183,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { {authority1, commonpb.AccountType_BUCKET_1_KIN}, {authority2, commonpb.AccountType_BUCKET_10_KIN}, } { - timelockAccounts, err := authorityAndType.account.GetTimelockAccounts(coreMintAccount) + timelockAccounts, err := authorityAndType.account.GetTimelockAccounts(vmAccount, coreMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -203,7 +206,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { {authority3, "app1.com"}, {authority4, "app2.com"}, } { - timelockAccounts, err := authorityAndRelationship.account.GetTimelockAccounts(coreMintAccount) + timelockAccounts, err := authorityAndRelationship.account.GetTimelockAccounts(vmAccount, coreMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() diff --git a/pkg/code/common/subsidizer.go b/pkg/code/common/subsidizer.go index 595bfbe7..19ce796a 100644 --- a/pkg/code/common/subsidizer.go +++ b/pkg/code/common/subsidizer.go @@ -49,7 +49,7 @@ var ( fulfillment.UploadCommitmentProof: 5000, // 0.000005 SOL (5000 lamports per signature) fulfillment.VerifyCommitmentProof: 5000, // 0.000005 SOL (5000 lamports per signature) fulfillment.OpenCommitmentVault: 2050000, // 0.00205 SOL - fulfillment.CloseCommitmentVault: 5000, // 0.000005 SOL (5000 lamports per signature) + fulfillment.CloseCommitment: 5000, // 0.000005 SOL (5000 lamports per signature) } lamportsPerCreateNonceAccount uint64 = 1450000 // 0.00145 SOL ) diff --git a/pkg/code/data/action/action.go b/pkg/code/data/action/action.go index d3c9baae..166f0993 100644 --- a/pkg/code/data/action/action.go +++ b/pkg/code/data/action/action.go @@ -4,8 +4,8 @@ import ( "errors" "time" - "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/pointer" ) type Type uint8 @@ -14,7 +14,7 @@ const ( UnknownType Type = iota OpenAccount CloseEmptyAccount - CloseDormantAccount + CloseDormantAccount // Deprecated by the VM NoPrivacyTransfer NoPrivacyWithdraw PrivateTransfer // Incorprorates all client-side private movement of funds. Backend processes don't care about the distinction, yet. diff --git a/pkg/code/data/fulfillment/fulfillment.go b/pkg/code/data/fulfillment/fulfillment.go index 391b9605..5936dfea 100644 --- a/pkg/code/data/fulfillment/fulfillment.go +++ b/pkg/code/data/fulfillment/fulfillment.go @@ -5,10 +5,10 @@ import ( "github.com/pkg/errors" - "github.com/code-payments/code-server/pkg/phone" - "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/phone" + "github.com/code-payments/code-server/pkg/pointer" ) var ( @@ -27,14 +27,14 @@ const ( TemporaryPrivacyTransferWithAuthority PermanentPrivacyTransferWithAuthority TransferWithCommitment - CloseEmptyTimelockAccount - CloseDormantTimelockAccount + CloseEmptyTimelockAccount // Technically a compression with the new VM flows + CloseDormantTimelockAccount // Deprecated by the VM SaveRecentRoot - InitializeCommitmentProof - UploadCommitmentProof - VerifyCommitmentProof // Deprecated, since we bundle verification with OpenCommitmentVault - OpenCommitmentVault - CloseCommitmentVault + InitializeCommitmentProof // Deprecated with new VM flows + UploadCommitmentProof // Deprecated with new VM flows + VerifyCommitmentProof // Deprecated with new VM flows + OpenCommitmentVault // Deprecated with new VM flows + CloseCommitment ) type State uint8 @@ -63,6 +63,13 @@ type Record struct { Nonce *string Blockhash *string + // todo: For virtual instructions, assumes a single one per fulfillment. + // We'll need new modelling when we get around to batching virtual + // instructions, but we're starting with the easiest implementation. + VirtualSignature *string + VirtualNonce *string + VirtualBlockhash *string + Source string // Source token account involved in the transaction Destination *string // Destination token account involved in the transaction, when it makes sense (eg. transfers) @@ -141,6 +148,9 @@ func (r *Record) Clone() Record { Signature: pointer.StringCopy(r.Signature), Nonce: pointer.StringCopy(r.Nonce), Blockhash: pointer.StringCopy(r.Blockhash), + VirtualSignature: pointer.StringCopy(r.VirtualSignature), + VirtualNonce: pointer.StringCopy(r.VirtualNonce), + VirtualBlockhash: pointer.StringCopy(r.VirtualBlockhash), Source: r.Source, Destination: pointer.StringCopy(r.Destination), IntentOrderingIndex: r.IntentOrderingIndex, @@ -164,6 +174,9 @@ func (r *Record) CopyTo(dst *Record) { dst.Signature = r.Signature dst.Nonce = r.Nonce dst.Blockhash = r.Blockhash + dst.VirtualSignature = pointer.StringCopy(r.VirtualSignature) + dst.VirtualNonce = pointer.StringCopy(r.VirtualNonce) + dst.VirtualBlockhash = pointer.StringCopy(r.VirtualBlockhash) dst.Source = r.Source dst.Destination = r.Destination dst.IntentOrderingIndex = r.IntentOrderingIndex @@ -212,6 +225,22 @@ func (r *Record) Validate() error { return errors.New("nonce and blockhash must be set or not set at the same time") } + if r.VirtualSignature != nil && len(*r.VirtualSignature) == 0 { + return errors.New("virtual signature is required when set") + } + + if r.VirtualNonce != nil && len(*r.VirtualNonce) == 0 { + return errors.New("virtual nonce is required when set") + } + + if r.VirtualBlockhash != nil && len(*r.VirtualBlockhash) == 0 { + return errors.New("virtual blockhash is required when set") + } + + if (r.VirtualNonce == nil) != (r.VirtualBlockhash == nil) { + return errors.New("virtual nonce and virtual blockhash must be set or not set at the same time") + } + if len(r.Source) == 0 { return errors.New("source token account is required") } @@ -288,8 +317,8 @@ func (s Type) String() string { return "verify_commitment_proof" case OpenCommitmentVault: return "open_commitment_vault" - case CloseCommitmentVault: - return "close_commitment_vault" + case CloseCommitment: + return "close_commitment" } return "unknown" diff --git a/pkg/code/data/fulfillment/memory/store.go b/pkg/code/data/fulfillment/memory/store.go index 4618d465..0d64ebec 100644 --- a/pkg/code/data/fulfillment/memory/store.go +++ b/pkg/code/data/fulfillment/memory/store.go @@ -7,9 +7,9 @@ import ( "sync" "time" + "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" ) type store struct { @@ -56,6 +56,15 @@ func (s *store) findBySignature(sig string) *fulfillment.Record { return nil } +func (s *store) findByVirtualSignature(sig string) *fulfillment.Record { + for _, item := range s.records { + if item.VirtualSignature != nil && *item.VirtualSignature == sig { + return item + } + } + return nil +} + func (s *store) findByIntent(intent string) []*fulfillment.Record { res := make([]*fulfillment.Record, 0) for _, item := range s.records { @@ -494,10 +503,16 @@ func (s *store) Update(ctx context.Context, data *fulfillment.Record) error { return fulfillment.ErrFulfillmentNotFound } + item.Data = data.Data + item.Signature = pointer.StringCopy(data.Signature) item.Nonce = pointer.StringCopy(data.Nonce) item.Blockhash = pointer.StringCopy(data.Blockhash) - item.Data = data.Data + + item.VirtualSignature = pointer.StringCopy(data.VirtualSignature) + item.VirtualNonce = pointer.StringCopy(data.VirtualNonce) + item.VirtualBlockhash = pointer.StringCopy(data.VirtualBlockhash) + item.State = data.State if item.FulfillmentType == fulfillment.CloseDormantTimelockAccount { @@ -588,6 +603,18 @@ func (s *store) GetBySignature(ctx context.Context, sig string) (*fulfillment.Re return nil, fulfillment.ErrFulfillmentNotFound } +func (s *store) GetByVirtualSignature(ctx context.Context, sig string) (*fulfillment.Record, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if item := s.findByVirtualSignature(sig); item != nil { + cloned := item.Clone() + return &cloned, nil + } + + return nil, fulfillment.ErrFulfillmentNotFound +} + func (s *store) GetAllByState(ctx context.Context, state fulfillment.State, includeDisabledActiveScheduling bool, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*fulfillment.Record, error) { s.mu.Lock() defer s.mu.Unlock() diff --git a/pkg/code/data/fulfillment/postgres/model.go b/pkg/code/data/fulfillment/postgres/model.go index b579d195..bcc6667c 100644 --- a/pkg/code/data/fulfillment/postgres/model.go +++ b/pkg/code/data/fulfillment/postgres/model.go @@ -31,6 +31,9 @@ type fulfillmentModel struct { Signature sql.NullString `db:"signature"` Nonce sql.NullString `db:"nonce"` Blockhash sql.NullString `db:"blockhash"` + VirtualSignature sql.NullString `db:"virtual_signature"` + VirtualNonce sql.NullString `db:"virtual_nonce"` + VirtualBlockhash sql.NullString `db:"virtual_blockhash"` Source string `db:"source"` Destination sql.NullString `db:"destination"` IntentOrderingIndex uint64 `db:"intent_ordering_index"` @@ -70,6 +73,24 @@ func toFulfillmentModel(obj *fulfillment.Record) (*fulfillmentModel, error) { blockHashValue.String = *obj.Blockhash } + var virtualSignatureValue sql.NullString + if obj.VirtualSignature != nil { + virtualSignatureValue.Valid = true + virtualSignatureValue.String = *obj.VirtualSignature + } + + var virtualNonceValue sql.NullString + if obj.VirtualNonce != nil { + virtualNonceValue.Valid = true + virtualNonceValue.String = *obj.VirtualNonce + } + + var virtualBlockHashValue sql.NullString + if obj.VirtualBlockhash != nil { + virtualBlockHashValue.Valid = true + virtualBlockHashValue.String = *obj.VirtualBlockhash + } + var destinationValue sql.NullString if obj.Destination != nil { destinationValue.Valid = true @@ -93,6 +114,9 @@ func toFulfillmentModel(obj *fulfillment.Record) (*fulfillmentModel, error) { Signature: signatureValue, Nonce: nonceValue, Blockhash: blockHashValue, + VirtualSignature: virtualSignatureValue, + VirtualNonce: virtualNonceValue, + VirtualBlockhash: virtualBlockHashValue, Source: obj.Source, Destination: destinationValue, IntentOrderingIndex: obj.IntentOrderingIndex, @@ -121,6 +145,21 @@ func fromFulfillmentModel(obj *fulfillmentModel) *fulfillment.Record { blockhash = &obj.Blockhash.String } + var virtualSig *string + if obj.VirtualSignature.Valid { + virtualSig = &obj.VirtualSignature.String + } + + var virtualNonce *string + if obj.VirtualNonce.Valid { + virtualNonce = &obj.VirtualNonce.String + } + + var virtualBlockhash *string + if obj.VirtualBlockhash.Valid { + virtualBlockhash = &obj.VirtualBlockhash.String + } + var destination *string if obj.Destination.Valid { destination = &obj.Destination.String @@ -142,6 +181,9 @@ func fromFulfillmentModel(obj *fulfillmentModel) *fulfillment.Record { Signature: sig, Nonce: nonce, Blockhash: blockhash, + VirtualSignature: virtualSig, + VirtualNonce: virtualNonce, + VirtualBlockhash: virtualBlockhash, Source: obj.Source, Destination: destination, IntentOrderingIndex: obj.IntentOrderingIndex, @@ -341,10 +383,13 @@ func (m *fulfillmentModel) dbUpdate(ctx context.Context, db *sqlx.DB) error { m.Blockhash, m.Data, m.State, + m.VirtualSignature, + m.VirtualNonce, + m.VirtualBlockhash, } if m.FulfillmentType == uint(fulfillment.CloseDormantTimelockAccount) { - preSortingUpdateStmt = ", intent_ordering_index = $7, action_ordering_index = $8, fulfillment_ordering_index = $9" + preSortingUpdateStmt = ", intent_ordering_index = $10, action_ordering_index = $11, fulfillment_ordering_index = $12" params = append( params, m.IntentOrderingIndex, @@ -354,10 +399,10 @@ func (m *fulfillmentModel) dbUpdate(ctx context.Context, db *sqlx.DB) error { } query := fmt.Sprintf(`UPDATE `+fulfillmentTableName+` - SET signature = $2, nonce = $3, blockhash = $4, data = $5, state = $6%s + SET signature = $2, nonce = $3, blockhash = $4, data = $5, state = $6, virtual_signature = $7, virtual_nonce = $8, virtual_blockhash = $9%s WHERE id = $1 RETURNING - id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at`, + id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at`, preSortingUpdateStmt, ) @@ -375,7 +420,7 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*fulfillmentModel) var res []*fulfillmentModel query := `WITH inserted AS (` - query += `INSERT INTO ` + fulfillmentTableName + ` (intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at, batch_insertion_id) VALUES ` + query += `INSERT INTO ` + fulfillmentTableName + ` (intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at, batch_insertion_id) VALUES ` var parameters []interface{} for i, model := range models { @@ -389,8 +434,8 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*fulfillmentModel) baseIndex := len(parameters) query += fmt.Sprintf( - `($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d)`, - baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7, baseIndex+8, baseIndex+9, baseIndex+10, baseIndex+11, baseIndex+12, baseIndex+13, baseIndex+14, baseIndex+15, baseIndex+16, baseIndex+17, baseIndex+18, baseIndex+19, + `($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d)`, + baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7, baseIndex+8, baseIndex+9, baseIndex+10, baseIndex+11, baseIndex+12, baseIndex+13, baseIndex+14, baseIndex+15, baseIndex+16, baseIndex+17, baseIndex+18, baseIndex+19, baseIndex+20, baseIndex+21, baseIndex+22, ) if i != len(models)-1 { @@ -410,6 +455,9 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*fulfillmentModel) model.Signature, model.Nonce, model.Blockhash, + model.VirtualSignature, + model.VirtualNonce, + model.VirtualBlockhash, model.Source, model.Destination, model.IntentOrderingIndex, @@ -423,7 +471,7 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*fulfillmentModel) ) } - query += ` RETURNING id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at, batch_insertion_id) ` + query += ` RETURNING id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at, batch_insertion_id) ` // Kind of hacky, but we don't really have a great PK for on demand transactions // that allows us to update the corresponding record that was passed in (for example, @@ -502,7 +550,7 @@ func dbGetById(ctx context.Context, db *sqlx.DB, id uint64) (*fulfillmentModel, res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE id = $1 LIMIT 1` @@ -521,7 +569,7 @@ func dbGetBySignature(ctx context.Context, db *sqlx.DB, signature string) (*fulf res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE signature = $1 LIMIT 1` @@ -533,10 +581,29 @@ func dbGetBySignature(ctx context.Context, db *sqlx.DB, signature string) (*fulf return res, nil } +func dbGetByVirtualSignature(ctx context.Context, db *sqlx.DB, signature string) (*fulfillmentModel, error) { + if len(signature) == 0 { + return nil, fulfillment.ErrFulfillmentNotFound + } + + res := &fulfillmentModel{} + + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + FROM ` + fulfillmentTableName + ` + WHERE virtual_signature = $1 + LIMIT 1` + + err := db.GetContext(ctx, res, query, signature) + if err != nil { + return nil, pgutil.CheckNoRows(err, fulfillment.ErrFulfillmentNotFound) + } + return res, nil +} + func dbGetAllByState(ctx context.Context, db *sqlx.DB, state fulfillment.State, includeDisabledActiveScheduling bool, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*fulfillmentModel, error) { res := []*fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE (state = $1 AND %s) ` @@ -565,7 +632,7 @@ func dbGetAllByState(ctx context.Context, db *sqlx.DB, state fulfillment.State, func dbGetAllByIntent(ctx context.Context, db *sqlx.DB, intent string, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*fulfillmentModel, error) { res := []*fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE (intent = $1) ` @@ -588,7 +655,7 @@ func dbGetAllByIntent(ctx context.Context, db *sqlx.DB, intent string, cursor q. func dbGetAllByAction(ctx context.Context, db *sqlx.DB, intentId string, actionId uint32) ([]*fulfillmentModel, error) { res := []*fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE (intent = $1 and action_id = $2) ` @@ -608,7 +675,7 @@ func dbGetAllByAction(ctx context.Context, db *sqlx.DB, intentId string, actionI func dbGetAllByTypeAndAction(ctx context.Context, db *sqlx.DB, fulfillmentType fulfillment.Type, intentId string, actionId uint32) ([]*fulfillmentModel, error) { res := []*fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE intent = $1 AND action_id = $2 AND fulfillment_type = $3 ` @@ -628,7 +695,7 @@ func dbGetAllByTypeAndAction(ctx context.Context, db *sqlx.DB, fulfillmentType f func dbGetFirstSchedulableByAddressAsSource(ctx context.Context, db *sqlx.DB, address string) (*fulfillmentModel, error) { res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE (source = $1 AND (state = $2 OR state = $3)) ORDER BY intent_ordering_index ASC, action_ordering_index ASC, fulfillment_ordering_index ASC @@ -644,7 +711,7 @@ func dbGetFirstSchedulableByAddressAsSource(ctx context.Context, db *sqlx.DB, ad func dbGetFirstSchedulableByAddressAsDestination(ctx context.Context, db *sqlx.DB, address string) (*fulfillmentModel, error) { res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE (destination = $1 AND (state = $2 OR state = $3)) ORDER BY intent_ordering_index ASC, action_ordering_index ASC, fulfillment_ordering_index ASC @@ -660,7 +727,7 @@ func dbGetFirstSchedulableByAddressAsDestination(ctx context.Context, db *sqlx.D func dbGetFirstSchedulableByType(ctx context.Context, db *sqlx.DB, fulfillmentType fulfillment.Type) (*fulfillmentModel, error) { res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE (fulfillment_type = $1 AND (state = $2 OR state = $3)) ORDER BY intent_ordering_index ASC, action_ordering_index ASC, fulfillment_ordering_index ASC @@ -676,7 +743,7 @@ func dbGetFirstSchedulableByType(ctx context.Context, db *sqlx.DB, fulfillmentTy func dbGetNextSchedulableByAddress(ctx context.Context, db *sqlx.DB, address string, intentOrderingIndex uint64, actionOrderingIndex, fulfillmentOrderingIndex uint32) (*fulfillmentModel, error) { res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at FROM ` + fulfillmentTableName + ` WHERE ((source = $1 OR destination = $1) AND (state = $2 OR state = $3) AND (intent_ordering_index > $4 OR (intent_ordering_index = $4 AND action_ordering_index > $5) OR (intent_ordering_index = $4 AND action_ordering_index = $5 AND fulfillment_ordering_index > $6))) ORDER BY intent_ordering_index ASC, action_ordering_index ASC, fulfillment_ordering_index ASC diff --git a/pkg/code/data/fulfillment/postgres/store.go b/pkg/code/data/fulfillment/postgres/store.go index 8bc63516..c33bd280 100644 --- a/pkg/code/data/fulfillment/postgres/store.go +++ b/pkg/code/data/fulfillment/postgres/store.go @@ -7,9 +7,9 @@ import ( "github.com/jmoiron/sqlx" + "github.com/code-payments/code-server/pkg/code/data/fulfillment" pgutil "github.com/code-payments/code-server/pkg/database/postgres" "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" ) type store struct { @@ -156,6 +156,16 @@ func (s *store) GetBySignature(ctx context.Context, signature string) (*fulfillm return fromFulfillmentModel(obj), nil } +// GetByVirtualSignature implements fulfillment.Store.GetByVirtualSignature +func (s *store) GetByVirtualSignature(ctx context.Context, signature string) (*fulfillment.Record, error) { + obj, err := dbGetByVirtualSignature(ctx, s.db, signature) + if err != nil { + return nil, err + } + + return fromFulfillmentModel(obj), nil +} + // GetAllByState implements fulfillment.Store.GetAllByState func (s *store) GetAllByState(ctx context.Context, state fulfillment.State, includeDisabledActiveScheduling bool, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*fulfillment.Record, error) { models, err := dbGetAllByState(ctx, s.db, state, includeDisabledActiveScheduling, cursor, limit, direction) diff --git a/pkg/code/data/fulfillment/postgres/store_test.go b/pkg/code/data/fulfillment/postgres/store_test.go index 142fe6e5..39da7720 100644 --- a/pkg/code/data/fulfillment/postgres/store_test.go +++ b/pkg/code/data/fulfillment/postgres/store_test.go @@ -35,6 +35,10 @@ const ( nonce TEXT NULL, blockhash TEXT NULL, + virtual_signature TEXT NULL, + virtual_nonce TEXT NULL, + virtual_blockhash TEXT NULL, + source TEXT NOT NULL, destination TEXT NULL, diff --git a/pkg/code/data/fulfillment/store.go b/pkg/code/data/fulfillment/store.go index 54345e0d..026ad7b5 100644 --- a/pkg/code/data/fulfillment/store.go +++ b/pkg/code/data/fulfillment/store.go @@ -59,6 +59,9 @@ type Store interface { // GetBySignature finds the fulfillment record for a given signature. GetBySignature(ctx context.Context, signature string) (*Record, error) + // GetByVirtualSignature finds the fulfillment record for a given virtual signature. + GetByVirtualSignature(ctx context.Context, signature string) (*Record, error) + // MarkAsActivelyScheduled marks a fulfillment as actively scheduled MarkAsActivelyScheduled(ctx context.Context, id uint64) error diff --git a/pkg/code/data/fulfillment/tests/tests.go b/pkg/code/data/fulfillment/tests/tests.go index 1c33db06..9f33ed82 100644 --- a/pkg/code/data/fulfillment/tests/tests.go +++ b/pkg/code/data/fulfillment/tests/tests.go @@ -10,11 +10,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/database/query" + "github.com/code-payments/code-server/pkg/pointer" ) func RunTests(t *testing.T, s fulfillment.Store, teardown func()) { @@ -45,6 +45,11 @@ func testRoundTrip(t *testing.T, s fulfillment.Store) { assert.Equal(t, fulfillment.ErrFulfillmentNotFound, err) assert.Nil(t, actual) + actual, err = s.GetByVirtualSignature(ctx, "test_virtual_signature") + require.Error(t, err) + assert.Equal(t, fulfillment.ErrFulfillmentNotFound, err) + assert.Nil(t, actual) + expected := fulfillment.Record{ Signature: pointer.String("test_signature"), Intent: "test_intent1", @@ -55,6 +60,9 @@ func testRoundTrip(t *testing.T, s fulfillment.Store) { Data: []byte("test_data"), Nonce: pointer.String("test_nonce"), Blockhash: pointer.String("test_blockhash"), + VirtualSignature: pointer.String("test_virtual_signature"), + VirtualNonce: pointer.String("test_virtual_nonce"), + VirtualBlockhash: pointer.String("test_virtual_blockhash"), Source: "test_source", Destination: pointer.String("test_destination"), IntentOrderingIndex: 1, @@ -75,6 +83,11 @@ func testRoundTrip(t *testing.T, s fulfillment.Store) { assert.EqualValues(t, 1, actual.Id) assertEquivalentRecords(t, actual, &cloned) + actual, err = s.GetByVirtualSignature(ctx, "test_virtual_signature") + require.NoError(t, err) + assert.EqualValues(t, 1, actual.Id) + assertEquivalentRecords(t, actual, &cloned) + actual, err = s.GetById(ctx, 2) assert.Equal(t, fulfillment.ErrFulfillmentNotFound, err) assert.Nil(t, actual) @@ -83,6 +96,10 @@ func testRoundTrip(t *testing.T, s fulfillment.Store) { assert.Equal(t, fulfillment.ErrFulfillmentNotFound, err) assert.Nil(t, actual) + actual, err = s.GetByVirtualSignature(ctx, "test_virtual_signature_2") + assert.Equal(t, fulfillment.ErrFulfillmentNotFound, err) + assert.Nil(t, actual) + assert.Equal(t, fulfillment.ErrFulfillmentExists, s.PutAll(ctx, &expected)) expected.Id = 0 assert.Equal(t, fulfillment.ErrFulfillmentExists, s.PutAll(ctx, &expected)) @@ -1055,6 +1072,9 @@ func assertEquivalentRecords(t *testing.T, obj1, obj2 *fulfillment.Record) { assert.EqualValues(t, obj1.Signature, obj2.Signature) assert.EqualValues(t, obj1.Nonce, obj2.Nonce) assert.EqualValues(t, obj1.Blockhash, obj2.Blockhash) + assert.EqualValues(t, obj1.VirtualSignature, obj2.VirtualSignature) + assert.EqualValues(t, obj1.VirtualNonce, obj2.VirtualNonce) + assert.EqualValues(t, obj1.VirtualBlockhash, obj2.VirtualBlockhash) assert.Equal(t, obj1.Source, obj2.Source) assert.EqualValues(t, obj1.Destination, obj2.Destination) assert.Equal(t, obj1.IntentOrderingIndex, obj2.IntentOrderingIndex) diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 82285b37..7d9a8a0f 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -186,6 +186,7 @@ type DatabaseData interface { // -------------------------------------------------------------------------------- GetFulfillmentById(ctx context.Context, id uint64) (*fulfillment.Record, error) GetFulfillmentBySignature(ctx context.Context, signature string) (*fulfillment.Record, error) + GetFulfillmentByVirtualSignature(ctx context.Context, signature string) (*fulfillment.Record, error) GetFulfillmentCount(ctx context.Context) (uint64, error) GetFulfillmentCountByState(ctx context.Context, state fulfillment.State) (uint64, error) GetFulfillmentCountByStateGroupedByType(ctx context.Context, state fulfillment.State) (map[fulfillment.Type]uint64, error) @@ -754,6 +755,9 @@ func (dp *DatabaseProvider) GetFulfillmentById(ctx context.Context, id uint64) ( func (dp *DatabaseProvider) GetFulfillmentBySignature(ctx context.Context, signature string) (*fulfillment.Record, error) { return dp.fulfillments.GetBySignature(ctx, signature) } +func (dp *DatabaseProvider) GetFulfillmentByVirtualSignature(ctx context.Context, signature string) (*fulfillment.Record, error) { + return dp.fulfillments.GetByVirtualSignature(ctx, signature) +} func (dp *DatabaseProvider) GetFulfillmentCount(ctx context.Context) (uint64, error) { return dp.fulfillments.Count(ctx) } diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index 88dc2778..ed49f6fc 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -1,18 +1,20 @@ package transaction import ( + "crypto/ed25519" "errors" "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" "github.com/code-payments/code-server/pkg/solana/memo" ) -var ( - // Should be equal to minimum bucket size - maxBurnAmount = kin.ToQuarks(1) -) +// todo: The argument sizes are blowing out of proportion, though there's likely +// a larger refactor going to happen anyways when we support batching of +// many virtual instructions into a single Solana transaction. + +// todo: Support external variation of transfers // MakeNoncedTransaction makes a transaction that's backed by a nonce. The returned // transaction is not signed. @@ -38,9 +40,12 @@ func MakeOpenAccountTransaction( nonce *common.Account, bh solana.Blockhash, + memory *common.Account, + accountIndex int, + timelockAccounts *common.TimelockAccounts, ) (solana.Transaction, error) { - initializeInstruction, err := timelockAccounts.GetInitializeInstruction() + initializeInstruction, err := timelockAccounts.GetInitializeInstruction(memory, uint16(accountIndex)) if err != nil { return solana.Transaction{}, err } @@ -51,145 +56,220 @@ func MakeOpenAccountTransaction( return MakeNoncedTransaction(nonce, bh, instructions...) } -func MakeCloseEmptyAccountTransaction( +func MakeCompressAccountTransaction( nonce *common.Account, bh solana.Blockhash, - timelockAccounts *common.TimelockAccounts, -) (solana.Transaction, error) { - burnDustInstruction, err := timelockAccounts.GetBurnDustWithAuthorityInstruction(maxBurnAmount) - if err != nil { - return solana.Transaction{}, err - } - closeInstruction, err := timelockAccounts.GetCloseAccountsInstruction() - if err != nil { - return solana.Transaction{}, err - } + vm *common.Account, + memory *common.Account, + accountIndex uint16, + storage *common.Account, +) (solana.Transaction, error) { + compressInstruction := cvm.NewSystemAccountCompressInstruction( + &cvm.SystemAccountCompressInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmMemory: memory.PublicKey().ToBytes(), + VmStorage: storage.PublicKey().ToBytes(), + }, + &cvm.SystemAccountCompressInstructionArgs{ + AccountIndex: accountIndex, + }, + ) - instructions := []solana.Instruction{ - burnDustInstruction, - closeInstruction, - } - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, bh, compressInstruction) } -func MakeCloseAccountWithBalanceTransaction( +func MakeInternalCloseAccountWithBalanceTransaction( nonce *common.Account, bh solana.Blockhash, + virtualSignature solana.Signature, + virtualNonce *common.Account, + virtualBlockhash solana.Blockhash, + + vm *common.Account, + nonceMemory *common.Account, + nonceIndex uint16, + sourceMemory *common.Account, + sourceIndex uint16, + destinationMemory *common.Account, + destinationIndex uint16, + source *common.TimelockAccounts, destination *common.Account, additionalMemo *string, ) (solana.Transaction, error) { - originalMemoInstruction, err := MakeKreMemoInstruction() - if err != nil { - return solana.Transaction{}, err - } + memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) + memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) + memoryCPublicKeyBytes := ed25519.PublicKey(destinationMemory.PublicKey().ToBytes()) + + transferVirtualIxn := cvm.NewVirtualInstruction( + common.GetSubsidizer().PublicKey().ToBytes(), + &cvm.VirtualDurableNonce{ + Address: virtualNonce.PublicKey().ToBytes(), + Nonce: cvm.Hash(virtualBlockhash), + }, + cvm.NewTimelockWithdrawInternalVirtualInstructionCtor( + &cvm.TimelockWithdrawInternalVirtualInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + VirtualTimelock: source.State.PublicKey().ToBytes(), + VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), + Owner: source.VaultOwner.PublicKey().ToBytes(), + Destination: destination.PublicKey().ToBytes(), + Mint: source.Mint.PublicKey().ToBytes(), + }, + &cvm.TimelockWithdrawInternalVirtualInstructionArgs{ + TimelockBump: source.StateBump, + Signature: cvm.Signature(virtualSignature), + }, + ), + ) - memoInstructions := []solana.Instruction{ - originalMemoInstruction, - } + execInstruction := cvm.NewVmExecInstruction( + &cvm.VmExecInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmMemA: &memoryAPublicKeyBytes, + VmMemB: &memoryBPublicKeyBytes, + VmMemC: &memoryCPublicKeyBytes, + }, + &cvm.VmExecInstructionArgs{ + Opcode: transferVirtualIxn.Opcode, + MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, + MemBanks: []uint8{0, 1, 2}, + Data: transferVirtualIxn.Data, + }, + ) + var instructions []solana.Instruction if additionalMemo != nil { - if len(*additionalMemo) == 0 { - return solana.Transaction{}, errors.New("additional memo is empty") - } - - additionalMemoInstruction := memo.Instruction(*additionalMemo) - memoInstructions = append(memoInstructions, additionalMemoInstruction) - } - - revokeLockInstruction, err := source.GetRevokeLockWithAuthorityInstruction() - if err != nil { - return solana.Transaction{}, err - } - - deactivateLockInstruction, err := source.GetDeactivateInstruction() - if err != nil { - return solana.Transaction{}, err + instructions = append(instructions, memo.Instruction(*additionalMemo)) } + instructions = append(instructions, execInstruction) - withdrawInstruction, err := source.GetWithdrawInstruction(destination) - if err != nil { - return solana.Transaction{}, err - } - - closeInstruction, err := source.GetCloseAccountsInstruction() - if err != nil { - return solana.Transaction{}, err - } - - instructions := append( - memoInstructions, - revokeLockInstruction, - deactivateLockInstruction, - withdrawInstruction, - closeInstruction, - ) return MakeNoncedTransaction(nonce, bh, instructions...) } -func MakeTransferWithAuthorityTransaction( +func MakeInternalTransferWithAuthorityTransaction( nonce *common.Account, bh solana.Blockhash, + virtualSignature solana.Signature, + virtualNonce *common.Account, + virtualBlockhash solana.Blockhash, + + vm *common.Account, + nonceMemory *common.Account, + nonceIndex uint16, + sourceMemory *common.Account, + sourceIndex uint16, + destinationMemory *common.Account, + destinationIndex uint16, + source *common.TimelockAccounts, destination *common.Account, - kinAmountInQuarks uint64, + kinAmountInQuarks uint32, ) (solana.Transaction, error) { - memoInstruction, err := MakeKreMemoInstruction() - if err != nil { - return solana.Transaction{}, err - } + memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) + memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) + memoryCPublicKeyBytes := ed25519.PublicKey(destinationMemory.PublicKey().ToBytes()) + + transferVirtualIxn := cvm.NewVirtualInstruction( + common.GetSubsidizer().PublicKey().ToBytes(), + &cvm.VirtualDurableNonce{ + Address: virtualNonce.PublicKey().ToBytes(), + Nonce: cvm.Hash(virtualBlockhash), + }, + cvm.NewTimelockTransferInternalVirtualInstructionCtor( + &cvm.TimelockTransferInternalVirtualInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + VirtualTimelock: source.State.PublicKey().ToBytes(), + VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), + Owner: source.VaultOwner.PublicKey().ToBytes(), + Destination: destination.PublicKey().ToBytes(), + }, + &cvm.TimelockTransferInternalVirtualInstructionArgs{ + TimelockBump: source.StateBump, + Amount: kinAmountInQuarks, + Signature: cvm.Signature(virtualSignature), + }, + ), + ) - transferWithAuthorityInstruction, err := source.GetTransferWithAuthorityInstruction(destination, kinAmountInQuarks) - if err != nil { - return solana.Transaction{}, err - } + execInstruction := cvm.NewVmExecInstruction( + &cvm.VmExecInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmMemA: &memoryAPublicKeyBytes, + VmMemB: &memoryBPublicKeyBytes, + VmMemC: &memoryCPublicKeyBytes, + }, + &cvm.VmExecInstructionArgs{ + Opcode: transferVirtualIxn.Opcode, + MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, + MemBanks: []uint8{0, 1, 2}, + Data: transferVirtualIxn.Data, + }, + ) - instructions := []solana.Instruction{ - memoInstruction, - transferWithAuthorityInstruction, - } - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, bh, execInstruction) } -func MakeTreasuryAdvanceTransaction( +func MakeInternalTreasuryAdvanceTransaction( nonce *common.Account, bh solana.Blockhash, + vm *common.Account, + accountMemory *common.Account, + accountIndex uint16, + relayMemory *common.Account, + relayIndex uint16, + treasuryPool *common.Account, treasuryPoolVault *common.Account, destination *common.Account, commitment *common.Account, - treasuryPoolBump uint8, - kinAmountInQuarks uint64, + kinAmountInQuarks uint32, transcript []byte, recentRoot []byte, ) (solana.Transaction, error) { - memoInstruction, err := MakeKreMemoInstruction() - if err != nil { - return solana.Transaction{}, err - } + relayPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) + relayVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) + memoryAPublicKeyBytes := ed25519.PublicKey(accountMemory.PublicKey().ToBytes()) + memoryBPublicKeyBytes := ed25519.PublicKey(relayMemory.PublicKey().ToBytes()) + + relayTransferInternalVirtualInstruction := cvm.NewVirtualInstruction( + common.GetSubsidizer().PublicKey().ToBytes(), + nil, + cvm.NewRelayTransferInternalVirtualInstructionCtor( + &cvm.RelayTransferInternalVirtualInstructionAccounts{}, + &cvm.RelayTransferInternalVirtualInstructionArgs{ + Transcript: cvm.Hash(transcript), + RecentRoot: cvm.Hash(recentRoot), + Commitment: cvm.Hash(commitment.PublicKey().ToBytes()), + Amount: kinAmountInQuarks, + }, + ), + ) - transferWithAuthorityInstruction, err := makeTransferWithCommitmentInstruction( - treasuryPool, - treasuryPoolVault, - destination, - commitment, - treasuryPoolBump, - kinAmountInQuarks, - transcript, - recentRoot, + execInstruction := cvm.NewVmExecInstruction( + &cvm.VmExecInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmMemA: &memoryAPublicKeyBytes, + VmOmnibus: &memoryBPublicKeyBytes, + VmRelay: &relayPublicKeyBytes, + VmRelayVault: &relayVaultPublicKeyBytes, + }, + &cvm.VmExecInstructionArgs{ + Opcode: relayTransferInternalVirtualInstruction.Opcode, + MemIndices: []uint16{accountIndex, relayIndex}, + MemBanks: []uint8{0, 1}, + Data: relayTransferInternalVirtualInstruction.Data, + }, ) - if err != nil { - return solana.Transaction{}, err - } - instructions := []solana.Instruction{ - memoInstruction, - transferWithAuthorityInstruction, - } - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, bh, execInstruction) } diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go index 82f8815e..bbb201a1 100644 --- a/pkg/solana/cvm/address.go +++ b/pkg/solana/cvm/address.go @@ -23,6 +23,7 @@ var ( VmProofAccountPrefix = []byte("vm_proof_account") VmRelayAccountPrefix = []byte("vm_relay_account") VmRelayVaultPrefix = []byte("vm_relay_vault") + VmStorageAccountPrefix = []byte("vm_storage_account") VmUnlockPdaAccountPrefix = []byte("vm_unlock_pda_account") VmWithdrawReceiptAccountPrefix = []byte("vm_withdraw_receipt_account") ) @@ -75,6 +76,21 @@ func GetMemoryAccountAddress(args *GetMemoryAccountAddressArgs) (ed25519.PublicK ) } +type GetStorageAccountAddressArgs struct { + Name string + Vm ed25519.PublicKey +} + +func GetStorageAccountAddress(args *GetMemoryAccountAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmStorageAccountPrefix, + []byte(args.Name), + args.Vm, + ) +} + type GetRelayAccountAddressArgs struct { Name string Vm ed25519.PublicKey diff --git a/pkg/solana/cvm/instructions_system_account_compress.go b/pkg/solana/cvm/instructions_system_account_compress.go new file mode 100644 index 00000000..c49f0f2c --- /dev/null +++ b/pkg/solana/cvm/instructions_system_account_compress.go @@ -0,0 +1,72 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +var SystemAccountCompressInstructionDiscriminator = []byte{ + 0x50, 0xc8, 0x0f, 0x6c, 0xb5, 0x1d, 0x7d, 0x9c, +} + +const ( + SystemAccountCompressInstructionArgsSize = 2 // account_index +) + +type SystemAccountCompressInstructionArgs struct { + AccountIndex uint16 +} + +type SystemAccountCompressInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey + VmStorage ed25519.PublicKey +} + +func NewSystemAccountCompressInstruction( + accounts *SystemAccountCompressInstructionAccounts, + args *SystemAccountCompressInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(SystemAccountCompressInstructionDiscriminator)+ + SystemAccountCompressInstructionArgsSize) + + putDiscriminator(data, SystemAccountCompressInstructionDiscriminator, &offset) + putUint16(data, args.AccountIndex, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmMemory, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmStorage, + IsWritable: true, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/instructions_vm_exec.go b/pkg/solana/cvm/instructions_vm_exec.go index ab4b28ae..4e6027c9 100644 --- a/pkg/solana/cvm/instructions_vm_exec.go +++ b/pkg/solana/cvm/instructions_vm_exec.go @@ -16,11 +16,10 @@ type VmExecArgsAndAccounts struct { } type VmExecInstructionArgs struct { - Opcode Opcode - MemIndices []uint16 - MemBanks []uint8 - SignatureIndex uint8 - Data []uint8 + Opcode Opcode + MemIndices []uint16 + MemBanks []uint8 + Data []uint8 } type VmExecInstructionAccounts struct { @@ -30,7 +29,6 @@ type VmExecInstructionAccounts struct { VmMemB *ed25519.PublicKey VmMemC *ed25519.PublicKey VmMemD *ed25519.PublicKey - VmUnlockPda *ed25519.PublicKey VmOmnibus *ed25519.PublicKey VmRelay *ed25519.PublicKey VmRelayVault *ed25519.PublicKey @@ -52,7 +50,6 @@ func NewVmExecInstruction( putOpcode(data, args.Opcode, &offset) putUint16Array(data, args.MemIndices, &offset) putUint8Array(data, args.MemBanks, &offset) - putUint8(data, args.SignatureIndex, &offset) putUint8Array(data, args.Data, &offset) var tokenProgram *ed25519.PublicKey @@ -98,11 +95,6 @@ func NewVmExecInstruction( IsWritable: true, IsSigner: false, }, - { - PublicKey: getOptionalAccountMetaAddress(accounts.VmUnlockPda), - IsWritable: false, - IsSigner: false, - }, { PublicKey: getOptionalAccountMetaAddress(accounts.VmOmnibus), IsWritable: true, @@ -141,6 +133,5 @@ func getVmExecInstructionArgSize(args *VmExecInstructionArgs) int { return (1 + // opcode 4 + 2*len(args.MemIndices) + // mem_indices 4 + len(args.MemBanks) + // mem_banks - 1 + // signature_index 4 + len(args.Data)) // data } diff --git a/pkg/solana/cvm/instructions_vm_storage_init.go b/pkg/solana/cvm/instructions_vm_storage_init.go new file mode 100644 index 00000000..d5447300 --- /dev/null +++ b/pkg/solana/cvm/instructions_vm_storage_init.go @@ -0,0 +1,84 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + MaxStorageAccountNameLength = 32 +) + +var VmStorageInitInstructionDiscriminator = []byte{ + 0x9d, 0xdf, 0x16, 0xd1, 0x0f, 0x24, 0xfe, 0x09, +} + +const ( + VmStorageInitInstructionArgsSize = (4 + MaxStorageAccountNameLength + // name + 1) // levels +) + +type VmStorageInitInstructionArgs struct { + Name string + Levels uint8 +} + +type VmStorageInitInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey + VmStorage ed25519.PublicKey +} + +func NewVmStorageInitInstruction( + accounts *VmStorageInitInstructionAccounts, + args *VmStorageInitInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(VmStorageInitInstructionDiscriminator)+ + VmStorageInitInstructionArgsSize) + + putDiscriminator(data, VmStorageInitInstructionDiscriminator, &offset) + putString(data, args.Name, &offset) + putUint8(data, args.Levels, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmStorage, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index 792b682b..8e7cd7ca 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -3,15 +3,14 @@ package cvm type Opcode uint8 const ( - OpcodeTimelockTransferInternal Opcode = 36 - OpcodeTimelockTransferExternal Opcode = 37 - OpcodeTimelockTransferRelay Opcode = 38 + OpcodeTimelockTransferToInternal Opcode = 10 + OpcodeTimelockTransferToExternal Opcode = 11 + OpcodeTimelockTransferToRelay Opcode = 12 + OpcodeTimelockWithdrawToInternal Opcode = 13 + OpcodeTimelockWithdrawToExternal Opcode = 14 - OpcodeTransferWithCommitmentInternal Opcode = 52 - OpcodeTransferWithCommitmentExternal Opcode = 53 - - OpcodeCompoundCloseEmptyAccount Opcode = 60 - OpcodeCompoundCloseAccountWithBalance Opcode = 61 + OpcodeSplitterTransferToInternal Opcode = 20 + OpcodeSplitterTransferToExternal Opcode = 21 ) func putOpcode(dst []byte, v Opcode, offset *int) { diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go index 1caa0616..6b4049a5 100644 --- a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go +++ b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go @@ -34,6 +34,6 @@ func NewRelayTransferExternalVirtualInstructionCtor( putHash(data, args.RecentRoot, &offset) putHash(data, args.Commitment, &offset) - return OpcodeTransferWithCommitmentExternal, nil, data + return OpcodeSplitterTransferToExternal, nil, data } } diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go index 231243bf..891114c5 100644 --- a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go +++ b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go @@ -34,6 +34,6 @@ func NewRelayTransferInternalVirtualInstructionCtor( putHash(data, args.RecentRoot, &offset) putHash(data, args.Commitment, &offset) - return OpcodeTransferWithCommitmentInternal, nil, data + return OpcodeSplitterTransferToInternal, nil, data } } diff --git a/pkg/solana/cvm/virtual_instructions_timelock_close_empty.go b/pkg/solana/cvm/virtual_instructions_timelock_close_empty.go deleted file mode 100644 index 72b7f826..00000000 --- a/pkg/solana/cvm/virtual_instructions_timelock_close_empty.go +++ /dev/null @@ -1,69 +0,0 @@ -package cvm - -import ( - "crypto/ed25519" - - "github.com/code-payments/code-server/pkg/solana" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" -) - -const ( - TimelockCloseEmptyVirtrualInstructionDataSize = (SignatureSize + // signature - 4) // max_amount -) - -type TimelockCloseEmptyVirtualInstructionArgs struct { - TimelockBump uint8 - MaxAmount uint32 - Signature Signature -} - -type TimelockCloseEmptyVirtualInstructionAccounts struct { - VmAuthority ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - VirtualTimelockVault ed25519.PublicKey - Owner ed25519.PublicKey - Mint ed25519.PublicKey -} - -func NewTimelockCloseEmptyVirtualInstructionCtor( - accounts *TimelockCloseEmptyVirtualInstructionAccounts, - args *TimelockCloseEmptyVirtualInstructionArgs, -) VirtualInstructionCtor { - return func() (Opcode, []solana.Instruction, []byte) { - var offset int - data := make([]byte, TimelockCloseEmptyVirtrualInstructionDataSize) - putSignature(data, args.Signature, &offset) - putUint32(data, args.MaxAmount, &offset) - - ixns := []solana.Instruction{ - timelock_token.NewBurnDustWithAuthorityInstruction( - &timelock_token.BurnDustWithAuthorityInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - VaultOwner: accounts.Owner, - TimeAuthority: accounts.VmAuthority, - Mint: accounts.Mint, - Payer: accounts.VmAuthority, - }, - &timelock_token.BurnDustWithAuthorityInstructionArgs{ - TimelockBump: args.TimelockBump, - MaxAmount: uint64(args.MaxAmount), - }, - ).ToLegacyInstruction(), - timelock_token.NewCloseAccountsInstruction( - &timelock_token.CloseAccountsInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - CloseAuthority: accounts.VmAuthority, - Payer: accounts.VmAuthority, - }, - &timelock_token.CloseAccountsInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - } - - return OpcodeCompoundCloseEmptyAccount, ixns, data - } -} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go index a2de3eaf..833115f8 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go @@ -54,6 +54,6 @@ func NewTimelockTransferExternalVirtualInstructionCtor( ).ToLegacyInstruction(), } - return OpcodeTimelockTransferExternal, ixns, data + return OpcodeTimelockTransferToExternal, ixns, data } } diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go index 99910305..59ac781b 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go @@ -54,6 +54,6 @@ func NewTimelockTransferInternalVirtualInstructionCtor( ).ToLegacyInstruction(), } - return OpcodeTimelockTransferInternal, ixns, data + return OpcodeTimelockTransferToInternal, ixns, data } } diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go index d5e15d8c..00a7d4eb 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go @@ -54,6 +54,6 @@ func NewTimelockTransferRelayVirtualInstructionCtor( ).ToLegacyInstruction(), } - return OpcodeTimelockTransferRelay, ixns, data + return OpcodeTimelockTransferToRelay, ixns, data } } diff --git a/pkg/solana/cvm/virtual_instructions_timelock_close_account_with_balance.go b/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go similarity index 80% rename from pkg/solana/cvm/virtual_instructions_timelock_close_account_with_balance.go rename to pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go index 3840e04b..8a576727 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_close_account_with_balance.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go @@ -8,15 +8,15 @@ import ( ) const ( - TimelockCloseAccountWithBalanceVirtrualInstructionDataSize = SignatureSize // signature + TimelockWithdrawEnternalVirtrualInstructionDataSize = SignatureSize // signature ) -type TimelockCloseAccountWithBalanceVirtualInstructionArgs struct { +type TimelockWithdrawEnternalVirtualInstructionArgs struct { TimelockBump uint8 Signature Signature } -type TimelockCloseAccountWithBalanceVirtualInstructionAccounts struct { +type TimelockWithdrawEnternalVirtualInstructionAccounts struct { VmAuthority ed25519.PublicKey VirtualTimelock ed25519.PublicKey VirtualTimelockVault ed25519.PublicKey @@ -25,13 +25,13 @@ type TimelockCloseAccountWithBalanceVirtualInstructionAccounts struct { Mint ed25519.PublicKey } -func NewTimelockCloseAccountWithBalanceVirtualInstructionCtor( - accounts *TimelockCloseAccountWithBalanceVirtualInstructionAccounts, - args *TimelockCloseAccountWithBalanceVirtualInstructionArgs, +func NewTimelockWithdrawEnternalVirtualInstructionCtor( + accounts *TimelockWithdrawEnternalVirtualInstructionAccounts, + args *TimelockWithdrawEnternalVirtualInstructionArgs, ) VirtualInstructionCtor { return func() (Opcode, []solana.Instruction, []byte) { var offset int - data := make([]byte, TimelockCloseAccountWithBalanceVirtrualInstructionDataSize) + data := make([]byte, TimelockWithdrawEnternalVirtrualInstructionDataSize) putSignature(data, args.Signature, &offset) ixns := []solana.Instruction{ @@ -82,6 +82,6 @@ func NewTimelockCloseAccountWithBalanceVirtualInstructionCtor( ).ToLegacyInstruction(), } - return OpcodeCompoundCloseAccountWithBalance, ixns, data + return OpcodeTimelockWithdrawToExternal, ixns, data } } diff --git a/pkg/solana/cvm/virtual_instructions_timelock_withdraw_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_withdraw_internal.go new file mode 100644 index 00000000..90001c8c --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_timelock_withdraw_internal.go @@ -0,0 +1,87 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" +) + +const ( + TimelockWithdrawInternalVirtrualInstructionDataSize = SignatureSize // signature +) + +type TimelockWithdrawInternalVirtualInstructionArgs struct { + TimelockBump uint8 + Signature Signature +} + +type TimelockWithdrawInternalVirtualInstructionAccounts struct { + VmAuthority ed25519.PublicKey + VirtualTimelock ed25519.PublicKey + VirtualTimelockVault ed25519.PublicKey + Destination ed25519.PublicKey + Owner ed25519.PublicKey + Mint ed25519.PublicKey +} + +func NewTimelockWithdrawInternalVirtualInstructionCtor( + accounts *TimelockWithdrawInternalVirtualInstructionAccounts, + args *TimelockWithdrawInternalVirtualInstructionArgs, +) VirtualInstructionCtor { + return func() (Opcode, []solana.Instruction, []byte) { + var offset int + data := make([]byte, TimelockWithdrawInternalVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + + ixns := []solana.Instruction{ + newKreMemoIxn(), + timelock_token.NewRevokeLockWithAuthorityInstruction( + &timelock_token.RevokeLockWithAuthorityInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + TimeAuthority: accounts.VmAuthority, + Payer: accounts.VmAuthority, + }, + &timelock_token.RevokeLockWithAuthorityInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + timelock_token.NewDeactivateInstruction( + &timelock_token.DeactivateInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + VaultOwner: accounts.Owner, + Payer: accounts.VmAuthority, + }, + &timelock_token.DeactivateInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + timelock_token.NewWithdrawInstruction( + &timelock_token.WithdrawInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + VaultOwner: accounts.Owner, + Destination: accounts.Destination, + Payer: accounts.VmAuthority, + }, + &timelock_token.WithdrawInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + timelock_token.NewCloseAccountsInstruction( + &timelock_token.CloseAccountsInstructionAccounts{ + Timelock: accounts.VirtualTimelock, + Vault: accounts.VirtualTimelockVault, + CloseAuthority: accounts.VmAuthority, + Payer: accounts.VmAuthority, + }, + &timelock_token.CloseAccountsInstructionArgs{ + TimelockBump: args.TimelockBump, + }, + ).ToLegacyInstruction(), + } + + return OpcodeTimelockWithdrawToInternal, ixns, data + } +} From 7a0a24fc08dd0149220b36a5b32dbba43399aa44 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 7 Aug 2024 12:37:35 -0400 Subject: [PATCH 20/79] VM memory management (#163) * Sketch out simple VM ram management DB store with memory implementation * Add postgres implementation of VM ram memory management store * Allow unordered memory allocation results in tests * Add VM ram store to data provider * Add ability to free VM memory by virtual account address * Free memory when accounts are deleted through a fulfillment * Move on demand transaction creation into the DB transaction handler * Reserve VM memory for on demand fulfillments that create new accounts * Use indexer client to get virtual account memory locations in the sequencer --- pkg/code/async/nonce/service.go | 6 +- .../async/sequencer/fulfillment_handler.go | 104 ++++++--- pkg/code/async/sequencer/scheduler.go | 5 +- pkg/code/async/sequencer/service.go | 8 +- pkg/code/async/sequencer/vm.go | 93 ++++++++ pkg/code/async/sequencer/worker.go | 20 +- pkg/code/async/sequencer/worker_test.go | 3 +- pkg/code/data/internal.go | 29 +++ pkg/code/data/vm/ram/account.go | 95 ++++++++ pkg/code/data/vm/ram/memory/store.go | 156 +++++++++++++ pkg/code/data/vm/ram/memory/store_test.go | 15 ++ pkg/code/data/vm/ram/postgres/model.go | 213 ++++++++++++++++++ pkg/code/data/vm/ram/postgres/store.go | 55 +++++ pkg/code/data/vm/ram/postgres/store_test.go | 134 +++++++++++ pkg/code/data/vm/ram/store.go | 35 +++ pkg/code/data/vm/ram/tests/tests.go | 111 +++++++++ pkg/code/data/vm/ram/util.go | 18 ++ pkg/code/data/vm/ram/util_test.go | 58 +++++ pkg/code/transaction/transaction.go | 4 +- pkg/solana/cvm/virtual_accounts.go | 18 ++ 20 files changed, 1132 insertions(+), 48 deletions(-) create mode 100644 pkg/code/async/sequencer/vm.go create mode 100644 pkg/code/data/vm/ram/account.go create mode 100644 pkg/code/data/vm/ram/memory/store.go create mode 100644 pkg/code/data/vm/ram/memory/store_test.go create mode 100644 pkg/code/data/vm/ram/postgres/model.go create mode 100644 pkg/code/data/vm/ram/postgres/store.go create mode 100644 pkg/code/data/vm/ram/postgres/store_test.go create mode 100644 pkg/code/data/vm/ram/store.go create mode 100644 pkg/code/data/vm/ram/tests/tests.go create mode 100644 pkg/code/data/vm/ram/util.go create mode 100644 pkg/code/data/vm/ram/util_test.go create mode 100644 pkg/solana/cvm/virtual_accounts.go diff --git a/pkg/code/async/nonce/service.go b/pkg/code/async/nonce/service.go index ba4042d3..4d001c9d 100644 --- a/pkg/code/async/nonce/service.go +++ b/pkg/code/async/nonce/service.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" - indexperpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" + indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" "github.com/code-payments/code-server/pkg/code/async" code_data "github.com/code-payments/code-server/pkg/code/data" @@ -24,12 +24,12 @@ type service struct { log *logrus.Entry conf *conf data code_data.Provider - vmIndexerClient indexperpb.IndexerClient + vmIndexerClient indexerpb.IndexerClient rent uint64 } -func New(data code_data.Provider, vmIndexerClient indexperpb.IndexerClient, configProvider ConfigProvider) async.Service { +func New(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) async.Service { return &service{ log: logrus.StandardLogger().WithField("service", "nonce"), conf: configProvider(), diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 28974c60..232f18a1 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -8,6 +8,7 @@ import ( "time" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" + indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" commitment_worker "github.com/code-payments/code-server/pkg/code/async/commitment" "github.com/code-payments/code-server/pkg/code/common" @@ -19,6 +20,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/treasury" transaction_util "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" "github.com/code-payments/code-server/pkg/solana/token" ) @@ -127,8 +129,7 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) SupportsOnDemandTran } func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - var vm *common.Account // todo: configure vm account - var memory *common.Account // todo: configure memory account + var vm *common.Account // todo: configure vm account if fulfillmentRecord.FulfillmentType != fulfillment.InitializeLockedTimelockAccount { return nil, errors.New("invalid fulfillment type") @@ -149,12 +150,17 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransact return nil, err } + memory, accountIndex, err := reserveVmMemory(ctx, h.data, vm.PublicKey().ToBase58(), cvm.VirtualAccountTypeTimelock, fulfillmentRecord.Source) + if err != nil { + return nil, err + } + txn, err := transaction_util.MakeOpenAccountTransaction( selectedNonce.Account, selectedNonce.Blockhash, memory, - 0, // todo: reserve free space in the memory account + accountIndex, timelockAccounts, ) @@ -355,7 +361,7 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) OnSuccess(ctx context.Context, ful return err } - return nil + return onVirtualAccountDeleted(ctx, h.data, fulfillmentRecord.Source) } func (h *NoPrivacyWithdrawFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { @@ -615,12 +621,14 @@ func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) IsRevoked(ctx } type TransferWithCommitmentFulfillmentHandler struct { - data code_data.Provider + data code_data.Provider + vmIndexerClient indexerpb.IndexerClient } -func NewTransferWithCommitmentFulfillmentHandler(data code_data.Provider) FulfillmentHandler { +func NewTransferWithCommitmentFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { return &TransferWithCommitmentFulfillmentHandler{ - data: data, + data: data, + vmIndexerClient: vmIndexerClient, } } @@ -694,9 +702,7 @@ func (h *TransferWithCommitmentFulfillmentHandler) SupportsOnDemandTransactions( } func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - var vm *common.Account // todo: configure vm account - var accountMemory *common.Account // todo: configure memory account - var relayMemory *common.Account // todo: configure memory account + var vm *common.Account // todo: configure vm account commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) if err != nil { @@ -707,6 +713,15 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, errors.New("commitment in unexpected state") } + timelockRecord, err := h.data.GetTimelockByVault(ctx, commitmentRecord.Destination) + if err != nil { + return nil, err + } + destinationTimelockOwner, err := common.NewAccountFromPrivateKeyString(timelockRecord.VaultOwner) + if err != nil { + return nil, err + } + treasuryPool, err := common.NewAccountFromPublicKeyString(commitmentRecord.Pool) if err != nil { return nil, err @@ -737,16 +752,26 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, err } + timelockAccountMemory, timelockAccountIndex, err := getVirtualTimelockAccountLocationInMemory(ctx, h.vmIndexerClient, vm, destinationTimelockOwner) + if err != nil { + return nil, err + } + + relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, vm.PublicKey().ToBase58(), cvm.VirtualAccountTypeRelay, commitment.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + // todo: support external transfers txn, err := transaction_util.MakeInternalTreasuryAdvanceTransaction( selectedNonce.Account, selectedNonce.Blockhash, vm, - accountMemory, - 0, // todo: use indexer to find index + timelockAccountMemory, + timelockAccountIndex, relayMemory, - 0, // todo: use indexer to find index + relayAccountIndex, treasuryPool, treasuryPoolVault, @@ -801,12 +826,14 @@ func (h *TransferWithCommitmentFulfillmentHandler) IsRevoked(ctx context.Context } type CloseEmptyTimelockAccountFulfillmentHandler struct { - data code_data.Provider + data code_data.Provider + vmIndexerClient indexerpb.IndexerClient } -func NewCloseEmptyTimelockAccountFulfillmentHandler(data code_data.Provider) FulfillmentHandler { +func NewCloseEmptyTimelockAccountFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { return &CloseEmptyTimelockAccountFulfillmentHandler{ - data: data, + data: data, + vmIndexerClient: vmIndexerClient, } } @@ -846,20 +873,24 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) SupportsOnDemandTransactio func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { var vm *common.Account // todo: configure vm account - var memory *common.Account // todo: configure memory account var storage *common.Account // todo: configure storage account if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { return nil, errors.New("invalid fulfillment type") } + memory, index, err := getVirtualTimelockAccountLocationInMemory(ctx, h.vmIndexerClient, vm, nil) + if err != nil { + return nil, err + } + txn, err := transaction_util.MakeCompressAccountTransaction( selectedNonce.Account, selectedNonce.Blockhash, vm, memory, - 0, // todo: get account index + index, storage, ) if err != nil { @@ -879,7 +910,7 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnSuccess(ctx context.Cont return errors.New("invalid fulfillment type") } - return nil + return onVirtualAccountDeleted(ctx, h.data, fulfillmentRecord.Source) } func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { @@ -974,12 +1005,14 @@ func (h *SaveRecentRootFulfillmentHandler) IsRevoked(ctx context.Context, fulfil } type CloseCommitmentFulfillmentHandler struct { - data code_data.Provider + data code_data.Provider + vmIndexerClient indexerpb.IndexerClient } -func NewCloseCommitmentFulfillmentHandler(data code_data.Provider) FulfillmentHandler { +func NewCloseCommitmentFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { return &CloseCommitmentFulfillmentHandler{ - data: data, + data: data, + vmIndexerClient: vmIndexerClient, } } @@ -1000,20 +1033,29 @@ func (h *CloseCommitmentFulfillmentHandler) SupportsOnDemandTransactions() bool func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { var vm *common.Account // todo: configure vm account - var memory *common.Account // todo: configure memory account var storage *common.Account // todo: configure storage account if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { return nil, errors.New("invalid fulfillment type") } + relay, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) + if err != nil { + return nil, err + } + + memory, index, err := getVirtualRelayAccountLocationInMemory(ctx, h.vmIndexerClient, vm, relay) + if err != nil { + return nil, err + } + txn, err := transaction_util.MakeCompressAccountTransaction( selectedNonce.Account, selectedNonce.Blockhash, vm, memory, - 0, // todo: get account index + index, storage, ) if err != nil { @@ -1033,7 +1075,12 @@ func (h *CloseCommitmentFulfillmentHandler) OnSuccess(ctx context.Context, fulfi return errors.New("invalid fulfillment type") } - return markCommitmentClosed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) + err := markCommitmentClosed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) + if err != nil { + return err + } + + return onVirtualAccountDeleted(ctx, h.data, fulfillmentRecord.Source) } func (h *CloseCommitmentFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { @@ -1112,15 +1159,16 @@ func estimateTreasuryPoolFundingLevels(ctx context.Context, data code_data.Provi return total, used, nil } -func getFulfillmentHandlers(data code_data.Provider, configProvider ConfigProvider) map[fulfillment.Type]FulfillmentHandler { +// todo: simplify initialization of fulfillment handlers across service and contextual scheduler +func getFulfillmentHandlers(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) map[fulfillment.Type]FulfillmentHandler { handlersByType := make(map[fulfillment.Type]FulfillmentHandler) handlersByType[fulfillment.InitializeLockedTimelockAccount] = NewInitializeLockedTimelockAccountFulfillmentHandler(data) handlersByType[fulfillment.NoPrivacyTransferWithAuthority] = NewNoPrivacyTransferWithAuthorityFulfillmentHandler(data) handlersByType[fulfillment.NoPrivacyWithdraw] = NewNoPrivacyWithdrawFulfillmentHandler(data) handlersByType[fulfillment.TemporaryPrivacyTransferWithAuthority] = NewTemporaryPrivacyTransferWithAuthorityFulfillmentHandler(data, configProvider) handlersByType[fulfillment.PermanentPrivacyTransferWithAuthority] = NewPermanentPrivacyTransferWithAuthorityFulfillmentHandler(data, configProvider) - handlersByType[fulfillment.TransferWithCommitment] = NewTransferWithCommitmentFulfillmentHandler(data) - handlersByType[fulfillment.CloseEmptyTimelockAccount] = NewCloseEmptyTimelockAccountFulfillmentHandler(data) + handlersByType[fulfillment.TransferWithCommitment] = NewTransferWithCommitmentFulfillmentHandler(data, vmIndexerClient) + handlersByType[fulfillment.CloseEmptyTimelockAccount] = NewCloseEmptyTimelockAccountFulfillmentHandler(data, vmIndexerClient) handlersByType[fulfillment.SaveRecentRoot] = NewSaveRecentRootFulfillmentHandler(data) handlersByType[fulfillment.CloseCommitment] = NewCloseCommitmentFulfillmentHandler(data) return handlersByType diff --git a/pkg/code/async/sequencer/scheduler.go b/pkg/code/async/sequencer/scheduler.go index 322161a0..e82e6f5e 100644 --- a/pkg/code/async/sequencer/scheduler.go +++ b/pkg/code/async/sequencer/scheduler.go @@ -10,6 +10,7 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/fulfillment" + indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" ) // Scheduler decides when fulfillments can be scheduled for submission to the @@ -49,12 +50,12 @@ type contextualScheduler struct { // problem (likely a wavefunction collapse implementation). // 2. Fulfillments that require client signatures are validated to guarantee // success before being created. -func NewContextualScheduler(data code_data.Provider, configProvider ConfigProvider) Scheduler { +func NewContextualScheduler(data code_data.Provider, indexerClient indexerpb.IndexerClient, configProvider ConfigProvider) Scheduler { return &contextualScheduler{ log: logrus.StandardLogger().WithField("type", "sequencer/scheduler/contextual"), data: data, conf: configProvider(), - handlersByType: getFulfillmentHandlers(data, configProvider), + handlersByType: getFulfillmentHandlers(data, indexerClient, configProvider), includeSubsidizerChecks: true, } } diff --git a/pkg/code/async/sequencer/service.go b/pkg/code/async/sequencer/service.go index 32d031eb..cafe7637 100644 --- a/pkg/code/async/sequencer/service.go +++ b/pkg/code/async/sequencer/service.go @@ -7,6 +7,8 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" + indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" + "github.com/code-payments/code-server/pkg/code/async" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/action" @@ -25,18 +27,20 @@ type service struct { conf *conf data code_data.Provider scheduler Scheduler + vmIndexerClient indexerpb.IndexerClient fulfillmentHandlersByType map[fulfillment.Type]FulfillmentHandler actionHandlersByType map[action.Type]ActionHandler intentHandlersByType map[intent.Type]IntentHandler } -func New(data code_data.Provider, scheduler Scheduler, configProvider ConfigProvider) async.Service { +func New(data code_data.Provider, scheduler Scheduler, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) async.Service { return &service{ log: logrus.StandardLogger().WithField("service", "sequencer"), conf: configProvider(), data: data, scheduler: scheduler, - fulfillmentHandlersByType: getFulfillmentHandlers(data, configProvider), + vmIndexerClient: vmIndexerClient, + fulfillmentHandlersByType: getFulfillmentHandlers(data, vmIndexerClient, configProvider), actionHandlersByType: getActionHandlers(data), intentHandlersByType: getIntentHandlers(data), } diff --git a/pkg/code/async/sequencer/vm.go b/pkg/code/async/sequencer/vm.go new file mode 100644 index 00000000..2e4e86ba --- /dev/null +++ b/pkg/code/async/sequencer/vm.go @@ -0,0 +1,93 @@ +package async_sequencer + +import ( + "context" + "sync" + + "github.com/pkg/errors" + + indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" + + "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +var ( + // Global VM memory lock + // + // todo: Use a distributed lock + vmMemoryLock sync.Mutex +) + +func reserveVmMemory(ctx context.Context, data code_data.Provider, vm string, accountType cvm.VirtualAccountType, address string) (*common.Account, uint16, error) { + vmMemoryLock.Lock() + defer vmMemoryLock.Unlock() + + memoryAccountAddress, index, err := data.ReserveVmMemory(ctx, vm, accountType, address) + if err != nil { + return nil, 0, err + } + + memoryAccount, err := common.NewAccountFromPublicKeyString(memoryAccountAddress) + if err != nil { + return nil, 0, err + } + + return memoryAccount, index, nil +} + +// This method can be safely called multiple times, since we know "deleted" accounts +// will never be reopened or uncompressed back into memory +func onVirtualAccountDeleted(ctx context.Context, data code_data.Provider, address string) error { + err := data.FreeVmMemoryByAddress(ctx, address) + if err == ram.ErrNotReserved { + return nil + } + return err +} + +func getVirtualTimelockAccountLocationInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, owner *common.Account) (*common.Account, uint16, error) { + resp, err := vmIndexerClient.GetVirtualTimelockAccounts(ctx, &indexerpb.GetVirtualTimelockAccountsRequest{ + VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()}, + Owner: &indexerpb.Address{Value: owner.PublicKey().ToBytes()}, + }) + if err != nil { + return nil, 0, err + } else if resp.Result != indexerpb.GetVirtualTimelockAccountsResponse_OK { + return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) + } + + if len(resp.Items) > 1 { + return nil, 0, errors.New("multiple results returned") + } else if resp.Items[0].Storage.GetCompressed() != nil { + return nil, 0, errors.New("account is compressed") + } + + memory, err := common.NewAccountFromPublicKeyBytes(resp.Items[0].Storage.GetMemory().Account.Value) + if err != nil { + return nil, 0, err + } + + return memory, uint16(resp.Items[0].Storage.GetMemory().Index), nil +} + +func getVirtualRelayAccountLocationInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, relay *common.Account) (*common.Account, uint16, error) { + resp, err := vmIndexerClient.GetVirtualRelayAccount(ctx, &indexerpb.GetVirtualRelayAccountRequest{ + VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()}, + Address: &indexerpb.Address{Value: relay.PublicKey().ToBytes()}, + }) + if err != nil { + return nil, 0, err + } else if resp.Result != indexerpb.GetVirtualRelayAccountResponse_OK { + return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) + } + + memory, err := common.NewAccountFromPublicKeyBytes(resp.Item.Storage.GetMemory().Account.Value) + if err != nil { + return nil, 0, err + } + + return memory, uint16(resp.Item.Storage.GetMemory().Index), nil +} diff --git a/pkg/code/async/sequencer/worker.go b/pkg/code/async/sequencer/worker.go index ddf56cd7..b42d8b01 100644 --- a/pkg/code/async/sequencer/worker.go +++ b/pkg/code/async/sequencer/worker.go @@ -233,18 +233,18 @@ func (p *service) handlePending(ctx context.Context, record *fulfillment.Record) selectedNonce.Unlock() }() - txn, err := fulfillmentHandler.MakeOnDemandTransaction(ctx, record, selectedNonce) - if err != nil { - return err - } + err = p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { + txn, err := fulfillmentHandler.MakeOnDemandTransaction(ctx, record, selectedNonce) + if err != nil { + return err + } - record.Signature = pointer.String(base58.Encode(txn.Signature())) - record.Nonce = pointer.String(selectedNonce.Account.PublicKey().ToBase58()) - record.Blockhash = pointer.String(base58.Encode(selectedNonce.Blockhash[:])) - record.Data = txn.Marshal() + record.Signature = pointer.String(base58.Encode(txn.Signature())) + record.Nonce = pointer.String(selectedNonce.Account.PublicKey().ToBase58()) + record.Blockhash = pointer.String(base58.Encode(selectedNonce.Blockhash[:])) + record.Data = txn.Marshal() - err = p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { - err := selectedNonce.MarkReservedWithSignature(ctx, *record.Signature) + err = selectedNonce.MarkReservedWithSignature(ctx, *record.Signature) if err != nil { return err } diff --git a/pkg/code/async/sequencer/worker_test.go b/pkg/code/async/sequencer/worker_test.go index d1ef0df0..f52b5201 100644 --- a/pkg/code/async/sequencer/worker_test.go +++ b/pkg/code/async/sequencer/worker_test.go @@ -230,7 +230,8 @@ func setupWorkerEnv(t *testing.T) *workerTestEnv { actionHandler := &mockActionHandler{} intentHandler := &mockIntentHandler{} - worker := New(db, scheduler, withManualTestOverrides(&testOverrides{})).(*service) + // todo: setup a test vm indexer + worker := New(db, scheduler, nil, withManualTestOverrides(&testOverrides{})).(*service) for key := range worker.fulfillmentHandlersByType { worker.fulfillmentHandlersByType[key] = fulfillmentHandler } diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 7d9a8a0f..0781bf25 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -16,6 +16,7 @@ import ( currency_lib "github.com/code-payments/code-server/pkg/currency" pg "github.com/code-payments/code-server/pkg/database/postgres" "github.com/code-payments/code-server/pkg/database/query" + "github.com/code-payments/code-server/pkg/solana/cvm" timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" @@ -52,6 +53,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/user/identity" "github.com/code-payments/code-server/pkg/code/data/user/storage" "github.com/code-payments/code-server/pkg/code/data/vault" + vm_ram "github.com/code-payments/code-server/pkg/code/data/vm/ram" "github.com/code-payments/code-server/pkg/code/data/webhook" account_memory_client "github.com/code-payments/code-server/pkg/code/data/account/memory" @@ -87,6 +89,7 @@ import ( user_identity_memory_client "github.com/code-payments/code-server/pkg/code/data/user/identity/memory" user_storage_memory_client "github.com/code-payments/code-server/pkg/code/data/user/storage/memory" vault_memory_client "github.com/code-payments/code-server/pkg/code/data/vault/memory" + vm_ram_memory_client "github.com/code-payments/code-server/pkg/code/data/vm/ram/memory" webhook_memory_client "github.com/code-payments/code-server/pkg/code/data/webhook/memory" account_postgres_client "github.com/code-payments/code-server/pkg/code/data/account/postgres" @@ -121,6 +124,7 @@ import ( user_identity_postgres_client "github.com/code-payments/code-server/pkg/code/data/user/identity/postgres" user_storage_postgres_client "github.com/code-payments/code-server/pkg/code/data/user/storage/postgres" vault_postgres_client "github.com/code-payments/code-server/pkg/code/data/vault/postgres" + vm_ram_postgres_client "github.com/code-payments/code-server/pkg/code/data/vm/ram/postgres" webhook_postgres_client "github.com/code-payments/code-server/pkg/code/data/webhook/postgres" ) @@ -435,6 +439,13 @@ type DatabaseData interface { IsTweetProcessed(ctx context.Context, tweetId string) (bool, error) MarkTwitterNonceAsUsed(ctx context.Context, tweetId string, nonce uuid.UUID) error + // VM RAM + // -------------------------------------------------------------------------------- + InitializeVmMemory(ctx context.Context, record *vm_ram.Record) error + FreeVmMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error + FreeVmMemoryByAddress(ctx context.Context, address string) error + ReserveVmMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) + // ExecuteInTx executes fn with a single DB transaction that is scoped to the call. // This enables more complex transactions that can span many calls across the provider. // @@ -478,6 +489,7 @@ type DatabaseProvider struct { preferences preferences.Store airdrop airdrop.Store twitter twitter.Store + vmRam vm_ram.Store exchangeCache cache.Cache timelockCache cache.Cache @@ -540,6 +552,7 @@ func NewDatabaseProvider(dbConfig *pg.Config) (DatabaseData, error) { preferences: preferences_postgres_client.New(db), airdrop: airdrop_postgres_client.New(db), twitter: twitter_postgres_client.New(db), + vmRam: vm_ram_postgres_client.New(db), exchangeCache: cache.NewCache(maxExchangeRateCacheBudget), timelockCache: cache.NewCache(maxTimelockCacheBudget), @@ -583,6 +596,7 @@ func NewTestDatabaseProvider() DatabaseData { preferences: preferences_memory_client.New(), airdrop: airdrop_memory_client.New(), twitter: twitter_memory_client.New(), + vmRam: vm_ram_memory_client.New(), exchangeCache: cache.NewCache(maxExchangeRateCacheBudget), timelockCache: nil, // Shouldn't be used for tests @@ -1547,3 +1561,18 @@ func (dp *DatabaseProvider) IsTweetProcessed(ctx context.Context, tweetId string func (dp *DatabaseProvider) MarkTwitterNonceAsUsed(ctx context.Context, tweetId string, nonce uuid.UUID) error { return dp.twitter.MarkNonceAsUsed(ctx, tweetId, nonce) } + +// VM RAM +// -------------------------------------------------------------------------------- +func (dp *DatabaseProvider) InitializeVmMemory(ctx context.Context, record *vm_ram.Record) error { + return dp.vmRam.InitializeMemory(ctx, record) +} +func (dp *DatabaseProvider) FreeVmMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error { + return dp.vmRam.FreeMemoryByIndex(ctx, memoryAccount, index) +} +func (dp *DatabaseProvider) FreeVmMemoryByAddress(ctx context.Context, address string) error { + return dp.vmRam.FreeMemoryByAddress(ctx, address) +} +func (dp *DatabaseProvider) ReserveVmMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) { + return dp.vmRam.ReserveMemory(ctx, vm, accountType, address) +} diff --git a/pkg/code/data/vm/ram/account.go b/pkg/code/data/vm/ram/account.go new file mode 100644 index 00000000..cb7bd559 --- /dev/null +++ b/pkg/code/data/vm/ram/account.go @@ -0,0 +1,95 @@ +package ram + +import ( + "errors" + "time" + + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +type Record struct { + Id uint64 + + Vm string + + Address string + + Capacity uint16 + NumSectors uint8 + NumPages uint8 + PageSize uint8 + + StoredAccountType cvm.VirtualAccountType + + CreatedAt time.Time +} + +func (r *Record) Validate() error { + if len(r.Vm) == 0 { + return errors.New("vm is required") + } + + if len(r.Address) == 0 { + return errors.New("address is required") + } + + if r.Capacity == 0 { + return errors.New("capacity is required") + } + + if r.NumSectors == 0 { + return errors.New("sector count is required") + } + + if r.NumPages == 0 { + return errors.New("pages count is required") + } + + switch r.StoredAccountType { + case cvm.VirtualAccountTypeDurableNonce, cvm.VirtualAccountTypeRelay, cvm.VirtualAccountTypeTimelock: + default: + return errors.New("invalid stored account type") + } + + if r.PageSize == 0 { + return errors.New("page size is required") + } + + return nil +} + +func (r *Record) Clone() Record { + return Record{ + Id: r.Id, + + Vm: r.Vm, + + Address: r.Address, + + Capacity: r.Capacity, + NumSectors: r.NumSectors, + NumPages: r.NumPages, + PageSize: r.PageSize, + + StoredAccountType: r.StoredAccountType, + + CreatedAt: r.CreatedAt, + } +} + +func (r *Record) CopyTo(dst *Record) { + dst.Id = r.Id + + dst.Vm = r.Vm + + dst.Address = r.Address + + dst.Capacity = r.Capacity + dst.NumSectors = r.NumSectors + dst.NumPages = r.NumPages + dst.PageSize = r.PageSize + + dst.StoredAccountType = r.StoredAccountType + + dst.CreatedAt = r.CreatedAt +} diff --git a/pkg/code/data/vm/ram/memory/store.go b/pkg/code/data/vm/ram/memory/store.go new file mode 100644 index 00000000..f0c17bc0 --- /dev/null +++ b/pkg/code/data/vm/ram/memory/store.go @@ -0,0 +1,156 @@ +package memory + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +type store struct { + mu sync.Mutex + last uint64 + records []*ram.Record + reservedAccountIndices map[string]string + storedVirtualAccounts map[string]string +} + +// New returns a new in memory vm.ram.Store +func New() ram.Store { + return &store{ + reservedAccountIndices: make(map[string]string), + storedVirtualAccounts: make(map[string]string), + } +} + +// InitializeMemory implements vm.ram.Store.InitializeMemory +func (s *store) InitializeMemory(_ context.Context, record *ram.Record) error { + if err := record.Validate(); err != nil { + return err + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.last++ + if item := s.find(record); item != nil { + return ram.ErrAlreadyInitialized + } + + record.Id = s.last + if record.CreatedAt.IsZero() { + record.CreatedAt = time.Now() + } + + cloned := record.Clone() + s.records = append(s.records, &cloned) + + return nil +} + +// FreeMemoryByIndex implements vm.ram.Store.FreeMemoryByIndex +func (s *store) FreeMemoryByIndex(_ context.Context, memoryAccount string, index uint16) error { + s.mu.Lock() + defer s.mu.Unlock() + + reservationKey := getAccountIndexKey(memoryAccount, index) + address, ok := s.reservedAccountIndices[reservationKey] + if !ok { + return ram.ErrNotReserved + } + + delete(s.reservedAccountIndices, reservationKey) + delete(s.storedVirtualAccounts, address) + + return nil +} + +// FreeMemoryByAddress implements vm.ram.Store.FreeMemoryByAddress +func (s *store) FreeMemoryByAddress(_ context.Context, address string) error { + s.mu.Lock() + defer s.mu.Unlock() + + reservationKey, ok := s.storedVirtualAccounts[address] + if !ok { + return ram.ErrNotReserved + } + + delete(s.reservedAccountIndices, reservationKey) + delete(s.storedVirtualAccounts, address) + + return nil +} + +// ReserveMemory implements vm.ram.Store.ReserveMemory +func (s *store) ReserveMemory(_ context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.storedVirtualAccounts[address]; ok { + return "", 0, ram.ErrAddressAlreadyReserved + } + + items := s.findByVmAndAccountType(vm, accountType) + for _, item := range items { + actualCapacity := ram.GetActualCapcity(item) + for i := 0; i < int(actualCapacity); i++ { + reservationKey := getAccountIndexKey(item.Address, uint16(i)) + + if _, ok := s.reservedAccountIndices[reservationKey]; ok { + continue + } + + s.reservedAccountIndices[reservationKey] = address + s.storedVirtualAccounts[address] = reservationKey + return item.Address, uint16(i), nil + } + } + + return "", 0, ram.ErrNoFreeMemory +} + +func (s *store) find(data *ram.Record) *ram.Record { + for _, item := range s.records { + if item.Id == data.Id { + return item + } + + if item.Address == data.Address { + return item + } + } + + return nil +} + +func (s *store) findByVmAndAccountType(vm string, accountType cvm.VirtualAccountType) []*ram.Record { + var res []*ram.Record + for _, item := range s.records { + if item.Vm != vm { + continue + } + + if item.StoredAccountType != accountType { + continue + } + + res = append(res, item) + } + return res +} + +func (s *store) reset() { + s.mu.Lock() + defer s.mu.Unlock() + s.last = 0 + s.records = nil + s.reservedAccountIndices = make(map[string]string) + s.storedVirtualAccounts = make(map[string]string) +} + +func getAccountIndexKey(memoryAccount string, index uint16) string { + return fmt.Sprintf("%s:%d", memoryAccount, index) +} diff --git a/pkg/code/data/vm/ram/memory/store_test.go b/pkg/code/data/vm/ram/memory/store_test.go new file mode 100644 index 00000000..33e24fd2 --- /dev/null +++ b/pkg/code/data/vm/ram/memory/store_test.go @@ -0,0 +1,15 @@ +package memory + +import ( + "testing" + + "github.com/code-payments/code-server/pkg/code/data/vm/ram/tests" +) + +func TestVmRamMemoryStore(t *testing.T) { + testStore := New() + teardown := func() { + testStore.(*store).reset() + } + tests.RunTests(t, testStore, teardown) +} diff --git a/pkg/code/data/vm/ram/postgres/model.go b/pkg/code/data/vm/ram/postgres/model.go new file mode 100644 index 00000000..7209e407 --- /dev/null +++ b/pkg/code/data/vm/ram/postgres/model.go @@ -0,0 +1,213 @@ +package postgres + +import ( + "context" + "database/sql" + "time" + + "github.com/jmoiron/sqlx" + + "github.com/code-payments/code-server/pkg/code/data/vm/ram" + pgutil "github.com/code-payments/code-server/pkg/database/postgres" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +const ( + accountTableName = "codewallet__core_vmmemoryaccount" + allocatedMemoryTableName = "codewallet__core_vmmemoryallocatedmemory" +) + +type accountModel struct { + Id sql.NullInt64 `db:"id"` + + Vm string `db:"vm"` + + Address string `db:"address"` + + Capacity uint16 `db:"capacity"` + NumSectors uint8 `db:"num_sectors"` + NumPages uint8 `db:"num_pages"` + PageSize uint8 `db:"page_size"` + + StoredAccountType uint8 `db:"stored_account_type"` + + CreatedAt time.Time `db:"created_at"` +} + +type allocatedMemoryModel struct { + Id sql.NullInt64 `db:"id"` + + Vm string `db:"vm"` + + MemoryAccount string `db:"memory_account"` + Index uint16 `db:"index"` + IsAllocated bool `db:"is_allocated"` + StoredAccountType uint8 `db:"stored_account_type"` + Address sql.NullString `db:"address"` + + LastUpdatedAt time.Time `db:"last_updated_at"` +} + +func toAccountModel(obj *ram.Record) (*accountModel, error) { + if err := obj.Validate(); err != nil { + return nil, err + } + + return &accountModel{ + Vm: obj.Vm, + + Address: obj.Address, + + Capacity: obj.Capacity, + NumSectors: obj.NumSectors, + NumPages: obj.NumPages, + PageSize: obj.PageSize, + + StoredAccountType: uint8(obj.StoredAccountType), + + CreatedAt: obj.CreatedAt, + }, nil +} + +func fromAccountModel(obj *accountModel) *ram.Record { + return &ram.Record{ + Id: uint64(obj.Id.Int64), + + Vm: obj.Vm, + + Address: obj.Address, + + Capacity: obj.Capacity, + NumSectors: obj.NumSectors, + NumPages: obj.NumPages, + PageSize: obj.PageSize, + + StoredAccountType: cvm.VirtualAccountType(obj.StoredAccountType), + + CreatedAt: obj.CreatedAt, + } +} + +func (m *accountModel) dbInitialize(ctx context.Context, db *sqlx.DB) error { + return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { + query1 := `INSERT INTO ` + accountTableName + ` + (vm, address, capacity, num_sectors, num_pages, page_size, stored_account_type, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING + id, vm, address, capacity, num_sectors, num_pages, page_size, stored_account_type, created_at` + + if m.CreatedAt.IsZero() { + m.CreatedAt = time.Now() + } + + err := tx.QueryRowxContext( + ctx, + query1, + m.Vm, + m.Address, + m.Capacity, + m.NumSectors, + m.NumPages, + m.PageSize, + m.StoredAccountType, + m.CreatedAt, + ).StructScan(m) + + if err != nil { + return pgutil.CheckUniqueViolation(err, ram.ErrAlreadyInitialized) + } + + query2 := `INSERT INTO ` + allocatedMemoryTableName + ` + (vm, memory_account, index, is_allocated, stored_account_type, last_updated_at) + SELECT $1, $2, generate_series(0, $3), $4, $5, $6` + + _, err = tx.ExecContext( + ctx, + query2, + m.Vm, + m.Address, + ram.GetActualCapcity(fromAccountModel(m))-1, + false, + m.StoredAccountType, + m.CreatedAt, + ) + return err + }) +} + +func dbFreeMemoryByIndex(ctx context.Context, db *sqlx.DB, memoryAccount string, index uint16) error { + return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { + var model allocatedMemoryModel + + query := `UPDATE ` + allocatedMemoryTableName + ` + SET is_allocated = false, address = NULL, last_updated_at = $3 + WHERE memory_account = $1 and index = $2 AND is_allocated + RETURNING id, vm, memory_account, index, is_allocated, stored_account_type, address, last_updated_at` + + err := tx.QueryRowxContext( + ctx, + query, + memoryAccount, + index, + time.Now(), + ).StructScan(&model) + + return pgutil.CheckNoRows(err, ram.ErrNotReserved) + }) +} + +func dbFreeMemoryByAddress(ctx context.Context, db *sqlx.DB, address string) error { + return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { + var model allocatedMemoryModel + + query := `UPDATE ` + allocatedMemoryTableName + ` + SET is_allocated = false, address = NULL, last_updated_at = $2 + WHERE address = $1 AND is_allocated + RETURNING id, vm, memory_account, index, is_allocated, stored_account_type, address, last_updated_at` + + err := tx.QueryRowxContext( + ctx, + query, + address, + time.Now(), + ).StructScan(&model) + + return pgutil.CheckNoRows(err, ram.ErrNotReserved) + }) +} + +func dbReserveMemory(ctx context.Context, db *sqlx.DB, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) { + var memoryAccount string + var index uint16 + err := pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { + var model allocatedMemoryModel + + query := `UPDATE ` + allocatedMemoryTableName + ` + SET is_allocated = true, address = $3, last_updated_at = $4 + WHERE id IN ( + SELECT id FROM ` + allocatedMemoryTableName + ` + WHERE vm = $1 AND NOT is_allocated AND stored_account_type = $2 + LIMIT 1 + FOR UPDATE + ) + RETURNING id, vm, memory_account, index, is_allocated, stored_account_type, address, last_updated_at` + + err := tx.QueryRowxContext( + ctx, + query, + vm, + accountType, + address, + time.Now(), + ).StructScan(&model) + if err != nil { + return pgutil.CheckUniqueViolation(pgutil.CheckNoRows(err, ram.ErrNoFreeMemory), ram.ErrAddressAlreadyReserved) + } + + memoryAccount = model.MemoryAccount + index = model.Index + + return nil + }) + return memoryAccount, index, err +} diff --git a/pkg/code/data/vm/ram/postgres/store.go b/pkg/code/data/vm/ram/postgres/store.go new file mode 100644 index 00000000..b4e847a7 --- /dev/null +++ b/pkg/code/data/vm/ram/postgres/store.go @@ -0,0 +1,55 @@ +package postgres + +import ( + "context" + "database/sql" + + "github.com/jmoiron/sqlx" + + "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +type store struct { + db *sqlx.DB +} + +// New returns a new postgres vm.ram.Store +func New(db *sql.DB) ram.Store { + return &store{ + db: sqlx.NewDb(db, "pgx"), + } +} + +// InitializeMemory implements vm.ram.Store.InitializeMemory +func (s *store) InitializeMemory(ctx context.Context, record *ram.Record) error { + model, err := toAccountModel(record) + if err != nil { + return err + } + + err = model.dbInitialize(ctx, s.db) + if err != nil { + return err + } + + res := fromAccountModel(model) + res.CopyTo(record) + + return nil +} + +// FreeMemoryByIndex implements vm.ram.Store.FreeMemoryByIndex +func (s *store) FreeMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error { + return dbFreeMemoryByIndex(ctx, s.db, memoryAccount, index) +} + +// FreeMemoryByAddress implements vm.ram.Store.FreeMemoryByAddress +func (s *store) FreeMemoryByAddress(ctx context.Context, address string) error { + return dbFreeMemoryByAddress(ctx, s.db, address) +} + +// ReserveMemory implements vm.ram.Store.ReserveMemory +func (s *store) ReserveMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) { + return dbReserveMemory(ctx, s.db, vm, accountType, address) +} diff --git a/pkg/code/data/vm/ram/postgres/store_test.go b/pkg/code/data/vm/ram/postgres/store_test.go new file mode 100644 index 00000000..1359fff4 --- /dev/null +++ b/pkg/code/data/vm/ram/postgres/store_test.go @@ -0,0 +1,134 @@ +package postgres + +import ( + "database/sql" + "os" + "testing" + + "github.com/ory/dockertest/v3" + "github.com/sirupsen/logrus" + + "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/code/data/vm/ram/tests" + + postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" + + _ "github.com/jackc/pgx/v4/stdlib" +) + +var ( + testStore ram.Store + teardown func() +) + +const ( + // Used for testing ONLY, the table and migrations are external to this repository + tableCreate = ` + CREATE TABLE codewallet__core_vmmemoryaccount ( + id SERIAL NOT NULL PRIMARY KEY, + + vm TEXT NOT NULL, + + address TEXT NOT NULL, + + capacity INTEGER NOT NULL, + num_sectors INTEGER NOT NULL, + num_pages INTEGER NOT NULL, + page_size INTEGER NOT NULL, + + stored_account_type INTEGER NOT NULL, + + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + + CONSTRAINT codewallet__core_vmmemoryaccount__uniq__address UNIQUE (address) + ); + + CREATE TABLE codewallet__core_vmmemoryallocatedmemory ( + id SERIAL NOT NULL PRIMARY KEY, + + vm TEXT NOT NULL, + + memory_account TEXT NOT NULL, + index INTEGER NOT NULL, + is_allocated BOOL NOT NULL, + stored_account_type INTEGER NOT NULL, + address TEXT NULL, + + last_updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + + CONSTRAINT codewallet__core_vmmemoryallocatedmemory__uniq__memory_account__and__index UNIQUE (memory_account, index), + CONSTRAINT codewallet__core_vmmemoryallocatedmemory__uniq__address UNIQUE (address) + ); + ` + + // Used for testing ONLY, the table and migrations are external to this repository + tableDestroy = ` + DROP TABLE codewallet__core_vmmemoryaccount; + DROP TABLE codewallet__core_vmmemoryallocatedmemory; + ` +) + +func TestMain(m *testing.M) { + log := logrus.StandardLogger() + + testPool, err := dockertest.NewPool("") + if err != nil { + log.WithError(err).Error("Error creating docker pool") + os.Exit(1) + } + + var cleanUpFunc func() + db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) + if err != nil { + log.WithError(err).Error("Error starting postgres image") + os.Exit(1) + } + defer db.Close() + + if err := createTestTables(db); err != nil { + logrus.StandardLogger().WithError(err).Error("Error creating test tables") + cleanUpFunc() + os.Exit(1) + } + + testStore = New(db) + teardown = func() { + if pc := recover(); pc != nil { + cleanUpFunc() + panic(pc) + } + + if err := resetTestTables(db); err != nil { + logrus.StandardLogger().WithError(err).Error("Error resetting test tables") + cleanUpFunc() + os.Exit(1) + } + } + + code := m.Run() + cleanUpFunc() + os.Exit(code) +} + +func TestVmRamPostgresStore(t *testing.T) { + tests.RunTests(t, testStore, teardown) +} + +func createTestTables(db *sql.DB) error { + _, err := db.Exec(tableCreate) + if err != nil { + logrus.StandardLogger().WithError(err).Error("could not create test tables") + return err + } + return nil +} + +func resetTestTables(db *sql.DB) error { + _, err := db.Exec(tableDestroy) + if err != nil { + logrus.StandardLogger().WithError(err).Error("could not drop test tables") + return err + } + + return createTestTables(db) +} diff --git a/pkg/code/data/vm/ram/store.go b/pkg/code/data/vm/ram/store.go new file mode 100644 index 00000000..9dddcb36 --- /dev/null +++ b/pkg/code/data/vm/ram/store.go @@ -0,0 +1,35 @@ +package ram + +import ( + "context" + "errors" + + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +var ( + ErrAlreadyInitialized = errors.New("memory account already initalized") + ErrNoFreeMemory = errors.New("no available free memory") + ErrNotReserved = errors.New("memory is not reserved") + ErrAddressAlreadyReserved = errors.New("virtual account address already in memory") +) + +// Store implements a basic construct for managing RAM memory. For simplicity, +// it is assumed that each memory account will store a single account type, +// which eliminates any complexities with parallel transaction execution resulting +// in allocation errors due to free pages across sectors. +// +// Note: A lock outside this implementation is required to resolve any races. +type Store interface { + // Initializes a memory account for management + InitializeMemory(ctx context.Context, record *Record) error + + // FreeMemoryByIndex frees a piece of memory from a memory account at a particual index + FreeMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error + + // FreeMemoryByAddress frees a piece of memory assigned to the virtual account address + FreeMemoryByAddress(ctx context.Context, address string) error + + // ReserveMemory reserves a piece of memory in a VM for the virtual account address + ReserveMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) +} diff --git a/pkg/code/data/vm/ram/tests/tests.go b/pkg/code/data/vm/ram/tests/tests.go new file mode 100644 index 00000000..c2bf8458 --- /dev/null +++ b/pkg/code/data/vm/ram/tests/tests.go @@ -0,0 +1,111 @@ +package tests + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +func RunTests(t *testing.T, s ram.Store, teardown func()) { + for _, tf := range []func(t *testing.T, s ram.Store){ + testHappyPath, + } { + tf(t, s) + teardown() + } +} + +func testHappyPath(t *testing.T, s ram.Store) { + t.Run("testHappyPath", func(t *testing.T) { + ctx := context.Background() + + record1 := &ram.Record{ + Vm: "vm1", + Address: "memoryaccount1", + Capacity: 1000, + NumSectors: 2, + NumPages: 50, + PageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)), + StoredAccountType: cvm.VirtualAccountTypeTimelock, + } + + record2 := &ram.Record{ + Vm: "vm1", + Address: "memoryaccount2", + Capacity: 1000, + NumSectors: 2, + NumPages: 50, + PageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)) / 3, + StoredAccountType: cvm.VirtualAccountTypeTimelock, + } + + require.NoError(t, s.InitializeMemory(ctx, record1)) + require.NoError(t, s.InitializeMemory(ctx, record2)) + + assert.Equal(t, ram.ErrAlreadyInitialized, s.InitializeMemory(ctx, record1)) + + _, _, err := s.ReserveMemory(ctx, "vm1", cvm.VirtualAccountTypeDurableNonce, "virtualaccount") + assert.Equal(t, ram.ErrNoFreeMemory, err) + + _, _, err = s.ReserveMemory(ctx, "vm2", cvm.VirtualAccountTypeTimelock, "virtualaccount") + assert.Equal(t, ram.ErrNoFreeMemory, err) + + address1Indices := make(map[uint16]struct{}) + address2Indices := make(map[uint16]struct{}) + for i := 0; i < 300; i++ { + memoryAccount, index, err := s.ReserveMemory(ctx, "vm1", cvm.VirtualAccountTypeTimelock, fmt.Sprintf("virtualaccount%d", i)) + + if i < 100 { + require.NoError(t, err) + assert.Equal(t, "memoryaccount1", memoryAccount) + assert.True(t, index < 100) + _, ok := address1Indices[index] + assert.False(t, ok) + address1Indices[index] = struct{}{} + } else if i >= 100 && i < 124 { + require.NoError(t, err) + assert.Equal(t, "memoryaccount2", memoryAccount) + assert.True(t, index < 24) + _, ok := address2Indices[index] + assert.False(t, ok) + address2Indices[index] = struct{}{} + } else { + assert.Equal(t, ram.ErrNoFreeMemory, err) + } + } + + for i := 0; i < 10; i++ { + if i == 0 { + require.NoError(t, s.FreeMemoryByIndex(ctx, "memoryaccount1", 42)) + require.NoError(t, s.FreeMemoryByAddress(ctx, "virtualaccount66")) + } else { + assert.Equal(t, ram.ErrNotReserved, s.FreeMemoryByIndex(ctx, "memoryaccount1", 42)) + assert.Equal(t, ram.ErrNotReserved, s.FreeMemoryByAddress(ctx, "virtualaccount66")) + } + } + + memoryAccount, freedIndex1, err := s.ReserveMemory(ctx, "vm1", cvm.VirtualAccountTypeTimelock, "newvirtualaccount1") + require.NoError(t, err) + assert.Equal(t, "memoryaccount1", memoryAccount) + assert.True(t, freedIndex1 == 42 || freedIndex1 == 66) + + _, _, err = s.ReserveMemory(ctx, "vm1", cvm.VirtualAccountTypeTimelock, "newvirtualaccount1") + assert.Equal(t, ram.ErrAddressAlreadyReserved, err) + + memoryAccount, freedIndex2, err := s.ReserveMemory(ctx, "vm1", cvm.VirtualAccountTypeTimelock, "newvirtualaccount2") + require.NoError(t, err) + assert.Equal(t, "memoryaccount1", memoryAccount) + assert.True(t, freedIndex2 == 42 || freedIndex2 == 66) + + assert.NotEqual(t, freedIndex1, freedIndex2) + + _, _, err = s.ReserveMemory(ctx, "vm1", cvm.VirtualAccountTypeTimelock, "newvirtualaccount3") + assert.Equal(t, ram.ErrNoFreeMemory, err) + }) +} diff --git a/pkg/code/data/vm/ram/util.go b/pkg/code/data/vm/ram/util.go new file mode 100644 index 00000000..079cf64a --- /dev/null +++ b/pkg/code/data/vm/ram/util.go @@ -0,0 +1,18 @@ +package ram + +import ( + "math" + + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +func GetActualCapcity(record *Record) uint16 { + sizeInMemory := int(cvm.GetVirtualAccountSizeInMemory(record.StoredAccountType)) + pagesPerAccount := math.Ceil(1 / (float64(record.PageSize) / float64(sizeInMemory))) + availablePerSector := int(record.NumPages) / int(pagesPerAccount) + maxAvailableAcrossSectors := uint16(record.NumSectors) * uint16(availablePerSector) + if record.Capacity < maxAvailableAcrossSectors { + return record.Capacity + } + return maxAvailableAcrossSectors +} diff --git a/pkg/code/data/vm/ram/util_test.go b/pkg/code/data/vm/ram/util_test.go new file mode 100644 index 00000000..fa2f167c --- /dev/null +++ b/pkg/code/data/vm/ram/util_test.go @@ -0,0 +1,58 @@ +package ram + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +func TestGetActualCapcity(t *testing.T) { + for _, tc := range []struct { + capacity uint16 + numSectors uint8 + numPages uint8 + pageSize uint8 + expected uint16 + }{ + { + capacity: 1000, + numSectors: 2, + numPages: 50, + pageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)), + expected: 100, + }, + { + capacity: 10, + numSectors: 2, + numPages: 50, + pageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)), + expected: 10, + }, + { + capacity: 1000, + numSectors: 2, + numPages: 50, + pageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)) - 1, + expected: 50, + }, + { + capacity: 1000, + numSectors: 2, + numPages: 50, + pageSize: uint8(cvm.GetVirtualAccountSizeInMemory(cvm.VirtualAccountTypeTimelock)) / 3, + expected: 24, + }, + } { + record := &Record{ + Capacity: tc.capacity, + NumSectors: tc.numSectors, + NumPages: tc.numPages, + PageSize: tc.pageSize, + StoredAccountType: cvm.VirtualAccountTypeTimelock, + } + actual := GetActualCapcity(record) + assert.Equal(t, tc.expected, actual) + } +} diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index ed49f6fc..7975d563 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -41,11 +41,11 @@ func MakeOpenAccountTransaction( bh solana.Blockhash, memory *common.Account, - accountIndex int, + accountIndex uint16, timelockAccounts *common.TimelockAccounts, ) (solana.Transaction, error) { - initializeInstruction, err := timelockAccounts.GetInitializeInstruction(memory, uint16(accountIndex)) + initializeInstruction, err := timelockAccounts.GetInitializeInstruction(memory, accountIndex) if err != nil { return solana.Transaction{}, err } diff --git a/pkg/solana/cvm/virtual_accounts.go b/pkg/solana/cvm/virtual_accounts.go new file mode 100644 index 00000000..67e7f1ef --- /dev/null +++ b/pkg/solana/cvm/virtual_accounts.go @@ -0,0 +1,18 @@ +package cvm + +func GetVirtualAccountSize(accountType VirtualAccountType) uint32 { + switch accountType { + case VirtualAccountTypeDurableNonce: + return VirtualDurableNonceSize + case VirtualAccountTypeRelay: + return VirtualRelayAccountSize + case VirtualAccountTypeTimelock: + return VirtualTimelockAccountSize + default: + return 0 + } +} + +func GetVirtualAccountSizeInMemory(accountType VirtualAccountType) uint32 { + return GetVirtualAccountSize(accountType) + 1 +} From 4a4599f48745c39efa5e286d2b1367e3d619bf15 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 7 Aug 2024 12:43:01 -0400 Subject: [PATCH 21/79] Fix getFulfillmentHandlers --- pkg/code/async/sequencer/fulfillment_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 232f18a1..42622e15 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -1170,6 +1170,6 @@ func getFulfillmentHandlers(data code_data.Provider, vmIndexerClient indexerpb.I handlersByType[fulfillment.TransferWithCommitment] = NewTransferWithCommitmentFulfillmentHandler(data, vmIndexerClient) handlersByType[fulfillment.CloseEmptyTimelockAccount] = NewCloseEmptyTimelockAccountFulfillmentHandler(data, vmIndexerClient) handlersByType[fulfillment.SaveRecentRoot] = NewSaveRecentRootFulfillmentHandler(data) - handlersByType[fulfillment.CloseCommitment] = NewCloseCommitmentFulfillmentHandler(data) + handlersByType[fulfillment.CloseCommitment] = NewCloseCommitmentFulfillmentHandler(data, vmIndexerClient) return handlersByType } From e916cd175f27422f1a63ee8fe343440f7201fc63 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 7 Aug 2024 13:17:35 -0400 Subject: [PATCH 22/79] Support new memory account layouts (#164) --- pkg/solana/cvm/accounts_memory_account.go | 13 ++++--- pkg/solana/cvm/instructions_vm_memory_init.go | 7 +++- pkg/solana/cvm/types_memory_layout.go | 38 +++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 pkg/solana/cvm/types_memory_layout.go diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index 69f40058..03f1a658 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -13,17 +13,18 @@ const ( ) type MemoryAccountWithData struct { - Vm ed25519.PublicKey - Bump uint8 - Name string - Data PagedMemory + Vm ed25519.PublicKey + Bump uint8 + Name string + Layout MemoryLayout + Data PagedMemory } const MemoryAccountWithDataSize = (8 + // discriminator 32 + // vm 1 + // bump MaxMemoryAccountNameLength + // name - 1 + // padding + 1 + // layout PagedMemorySize) // data var MemoryAccountDiscriminator = []byte{0x89, 0x7a, 0xdc, 0x6e, 0xdd, 0xca, 0x3e, 0x7f} @@ -44,7 +45,7 @@ func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { getKey(data, &obj.Vm, &offset) getUint8(data, &obj.Bump, &offset) getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) - offset += 1 // padding + getMemoryLayout(data, &obj.Layout, &offset) getPagedMemory(data, &obj.Data, &offset) return nil diff --git a/pkg/solana/cvm/instructions_vm_memory_init.go b/pkg/solana/cvm/instructions_vm_memory_init.go index 1aac1011..39b12311 100644 --- a/pkg/solana/cvm/instructions_vm_memory_init.go +++ b/pkg/solana/cvm/instructions_vm_memory_init.go @@ -11,11 +11,13 @@ var VmMemoryInitInstructionDiscriminator = []byte{ } const ( - VmMemoryInitInstructionArgsSize = (4 + MaxMemoryAccountNameLength) // name + VmMemoryInitInstructionArgsSize = (4 + MaxMemoryAccountNameLength + // name + 1) // layout ) type VmMemoryInitInstructionArgs struct { - Name string + Name string + Layout MemoryLayout } type VmMemoryInitInstructionAccounts struct { @@ -37,6 +39,7 @@ func NewVmMemoryInitInstruction( putDiscriminator(data, VmMemoryInitInstructionDiscriminator, &offset) putString(data, args.Name, &offset) + putMemoryLayout(data, args.Layout, &offset) return solana.Instruction{ Program: PROGRAM_ADDRESS, diff --git a/pkg/solana/cvm/types_memory_layout.go b/pkg/solana/cvm/types_memory_layout.go new file mode 100644 index 00000000..85d6dd27 --- /dev/null +++ b/pkg/solana/cvm/types_memory_layout.go @@ -0,0 +1,38 @@ +package cvm + +type MemoryLayout uint8 + +const ( + MixedMemoryLayoutPageSize = 32 +) + +const ( + MemoryLayoutMixed MemoryLayout = iota + MemoryLayoutTimelock + MemoryLayoutNonce + MemoryLayoutRelay +) + +func putMemoryLayout(dst []byte, v MemoryLayout, offset *int) { + dst[*offset] = uint8(v) + *offset += 1 +} +func getMemoryLayout(src []byte, dst *MemoryLayout, offset *int) { + *dst = MemoryLayout(src[*offset]) + *offset += 1 +} + +func GetPageSizeFromMemoryLayout(layout MemoryLayout) uint32 { + switch layout { + case MemoryLayoutMixed: + return MixedMemoryLayoutPageSize + case MemoryLayoutTimelock: + return GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock) + case MemoryLayoutNonce: + return GetVirtualAccountSizeInMemory(VirtualDurableNonceSize) + case MemoryLayoutRelay: + return GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay) + default: + return 0 + } +} From 63bbb4cc32a7f624ce1f19fdeb9cdbe04c00e142 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 7 Aug 2024 16:11:09 -0400 Subject: [PATCH 23/79] Support VM compression with signature verification (#165) --- .../async/sequencer/fulfillment_handler.go | 8 ++-- pkg/code/async/sequencer/vm.go | 45 ++++++++++++++----- pkg/code/transaction/transaction.go | 4 ++ .../instructions_system_account_compress.go | 5 ++- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 42622e15..e2a6d321 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -752,7 +752,7 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, err } - timelockAccountMemory, timelockAccountIndex, err := getVirtualTimelockAccountLocationInMemory(ctx, h.vmIndexerClient, vm, destinationTimelockOwner) + _, timelockAccountMemory, timelockAccountIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vm, destinationTimelockOwner) if err != nil { return nil, err } @@ -879,7 +879,7 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ct return nil, errors.New("invalid fulfillment type") } - memory, index, err := getVirtualTimelockAccountLocationInMemory(ctx, h.vmIndexerClient, vm, nil) + virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vm, nil) if err != nil { return nil, err } @@ -892,6 +892,7 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ct memory, index, storage, + virtualAccountState.Marshal(), ) if err != nil { return nil, err @@ -1044,7 +1045,7 @@ func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context. return nil, err } - memory, index, err := getVirtualRelayAccountLocationInMemory(ctx, h.vmIndexerClient, vm, relay) + virtualAccountState, memory, index, err := getVirtualRelayAccountStateInMemory(ctx, h.vmIndexerClient, vm, relay) if err != nil { return nil, err } @@ -1057,6 +1058,7 @@ func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context. memory, index, storage, + virtualAccountState.Marshal(), ) if err != nil { return nil, err diff --git a/pkg/code/async/sequencer/vm.go b/pkg/code/async/sequencer/vm.go index 2e4e86ba..82e5203d 100644 --- a/pkg/code/async/sequencer/vm.go +++ b/pkg/code/async/sequencer/vm.go @@ -14,6 +14,8 @@ import ( "github.com/code-payments/code-server/pkg/solana/cvm" ) +// todo: some of these utilities likely belong in a more common package + var ( // Global VM memory lock // @@ -48,46 +50,65 @@ func onVirtualAccountDeleted(ctx context.Context, data code_data.Provider, addre return err } -func getVirtualTimelockAccountLocationInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, owner *common.Account) (*common.Account, uint16, error) { +func getVirtualTimelockAccountStateInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, owner *common.Account) (*cvm.VirtualTimelockAccount, *common.Account, uint16, error) { resp, err := vmIndexerClient.GetVirtualTimelockAccounts(ctx, &indexerpb.GetVirtualTimelockAccountsRequest{ VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()}, Owner: &indexerpb.Address{Value: owner.PublicKey().ToBytes()}, }) if err != nil { - return nil, 0, err + return nil, nil, 0, err } else if resp.Result != indexerpb.GetVirtualTimelockAccountsResponse_OK { - return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) + return nil, nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) } if len(resp.Items) > 1 { - return nil, 0, errors.New("multiple results returned") + return nil, nil, 0, errors.New("multiple results returned") } else if resp.Items[0].Storage.GetCompressed() != nil { - return nil, 0, errors.New("account is compressed") + return nil, nil, 0, errors.New("account is compressed") } memory, err := common.NewAccountFromPublicKeyBytes(resp.Items[0].Storage.GetMemory().Account.Value) if err != nil { - return nil, 0, err + return nil, nil, 0, err } - return memory, uint16(resp.Items[0].Storage.GetMemory().Index), nil + state := cvm.VirtualTimelockAccount{ + Owner: resp.Items[0].Account.Owner.Value, + Nonce: cvm.Hash(resp.Items[0].Account.Nonce.Value), + + TokenBump: uint8(resp.Items[0].Account.TokenBump), + UnlockBump: uint8(resp.Items[0].Account.UnlockBump), + WithdrawBump: uint8(resp.Items[0].Account.WithdrawBump), + + Balance: resp.Items[0].Account.Balance, + Bump: uint8(resp.Items[0].Account.Bump), + } + + return &state, memory, uint16(resp.Items[0].Storage.GetMemory().Index), nil } -func getVirtualRelayAccountLocationInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, relay *common.Account) (*common.Account, uint16, error) { +func getVirtualRelayAccountStateInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, relay *common.Account) (*cvm.VirtualRelayAccount, *common.Account, uint16, error) { resp, err := vmIndexerClient.GetVirtualRelayAccount(ctx, &indexerpb.GetVirtualRelayAccountRequest{ VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()}, Address: &indexerpb.Address{Value: relay.PublicKey().ToBytes()}, }) if err != nil { - return nil, 0, err + return nil, nil, 0, err } else if resp.Result != indexerpb.GetVirtualRelayAccountResponse_OK { - return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) + return nil, nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) } memory, err := common.NewAccountFromPublicKeyBytes(resp.Item.Storage.GetMemory().Account.Value) if err != nil { - return nil, 0, err + return nil, nil, 0, err + } + + state := cvm.VirtualRelayAccount{ + Address: resp.Item.Account.Address.Value, + Commitment: cvm.Hash(resp.Item.Account.Commitment.Value), + RecentRoot: cvm.Hash(resp.Item.Account.RecentRoot.Value), + Destination: resp.Item.Account.Destination.Value, } - return memory, uint16(resp.Item.Storage.GetMemory().Index), nil + return &state, memory, uint16(resp.Item.Storage.GetMemory().Index), nil } diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index 7975d563..a2232da5 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -64,7 +64,10 @@ func MakeCompressAccountTransaction( memory *common.Account, accountIndex uint16, storage *common.Account, + virtualAccountState []byte, ) (solana.Transaction, error) { + signature := ed25519.Sign(common.GetSubsidizer().PrivateKey().ToBytes(), virtualAccountState) + compressInstruction := cvm.NewSystemAccountCompressInstruction( &cvm.SystemAccountCompressInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), @@ -74,6 +77,7 @@ func MakeCompressAccountTransaction( }, &cvm.SystemAccountCompressInstructionArgs{ AccountIndex: accountIndex, + Signature: cvm.Signature(signature), }, ) diff --git a/pkg/solana/cvm/instructions_system_account_compress.go b/pkg/solana/cvm/instructions_system_account_compress.go index c49f0f2c..7fad2a93 100644 --- a/pkg/solana/cvm/instructions_system_account_compress.go +++ b/pkg/solana/cvm/instructions_system_account_compress.go @@ -11,11 +11,13 @@ var SystemAccountCompressInstructionDiscriminator = []byte{ } const ( - SystemAccountCompressInstructionArgsSize = 2 // account_index + SystemAccountCompressInstructionArgsSize = (2 + // account_index + SignatureSize) // signature ) type SystemAccountCompressInstructionArgs struct { AccountIndex uint16 + Signature Signature } type SystemAccountCompressInstructionAccounts struct { @@ -38,6 +40,7 @@ func NewSystemAccountCompressInstruction( putDiscriminator(data, SystemAccountCompressInstructionDiscriminator, &offset) putUint16(data, args.AccountIndex, &offset) + putSignature(data, args.Signature, &offset) return solana.Instruction{ Program: PROGRAM_ADDRESS, From c4b5d2981c916098d6242ccb725dd9418c76110b Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 8 Aug 2024 09:46:03 -0400 Subject: [PATCH 24/79] Ensure indexed Timelock account state has zero balance when closing empty account (#166) --- pkg/code/async/sequencer/fulfillment_handler.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index e2a6d321..244d2bbb 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -884,6 +884,10 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ct return nil, err } + if virtualAccountState.Balance != 0 { + return nil, errors.New("stale timelock account state") + } + txn, err := transaction_util.MakeCompressAccountTransaction( selectedNonce.Account, selectedNonce.Blockhash, From 7b767afc7143fd7262302787456286a7797d24fc Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 8 Aug 2024 11:11:46 -0400 Subject: [PATCH 25/79] Fix signature to be virtual account state hash for compression (#167) --- pkg/code/transaction/transaction.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index a2232da5..97e3316f 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -2,6 +2,7 @@ package transaction import ( "crypto/ed25519" + "crypto/sha256" "errors" "github.com/code-payments/code-server/pkg/code/common" @@ -66,7 +67,11 @@ func MakeCompressAccountTransaction( storage *common.Account, virtualAccountState []byte, ) (solana.Transaction, error) { - signature := ed25519.Sign(common.GetSubsidizer().PrivateKey().ToBytes(), virtualAccountState) + hasher := sha256.New() + hasher.Write(virtualAccountState) + hashedVirtualAccountState := hasher.Sum(nil) + + signature := ed25519.Sign(common.GetSubsidizer().PrivateKey().ToBytes(), hashedVirtualAccountState) compressInstruction := cvm.NewSystemAccountCompressInstruction( &cvm.SystemAccountCompressInstructionAccounts{ From 0193a1504439fbcad777e99d741b28458e47f3d2 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Tue, 13 Aug 2024 13:15:31 -0400 Subject: [PATCH 26/79] VM storage management (#168) * Add VM storage data store for initializing and getting account with minimum available capacity * Add ability to reserve storage in DB cache * Reserve VM storage when creating on demand transactions for deleting accounts * Implement simple service to dynamically spin up storage accounts when needed --- .../async/sequencer/fulfillment_handler.go | 38 +++- pkg/code/async/sequencer/vm.go | 29 ++- pkg/code/async/vm/service.go | 37 ++++ pkg/code/async/vm/storage.go | 144 +++++++++++++++ pkg/code/data/{vm => cvm}/ram/account.go | 0 pkg/code/data/{vm => cvm}/ram/memory/store.go | 12 +- .../data/{vm => cvm}/ram/memory/store_test.go | 2 +- .../data/{vm => cvm}/ram/postgres/model.go | 2 +- .../data/{vm => cvm}/ram/postgres/store.go | 12 +- .../{vm => cvm}/ram/postgres/store_test.go | 4 +- pkg/code/data/{vm => cvm}/ram/store.go | 0 pkg/code/data/{vm => cvm}/ram/tests/tests.go | 2 +- pkg/code/data/{vm => cvm}/ram/util.go | 0 pkg/code/data/{vm => cvm}/ram/util_test.go | 0 pkg/code/data/cvm/storage/account.go | 90 ++++++++++ pkg/code/data/cvm/storage/memory/store.go | 138 ++++++++++++++ .../data/cvm/storage/memory/store_test.go | 15 ++ pkg/code/data/cvm/storage/postgres/model.go | 169 ++++++++++++++++++ pkg/code/data/cvm/storage/postgres/store.go | 56 ++++++ .../data/cvm/storage/postgres/store_test.go | 127 +++++++++++++ pkg/code/data/cvm/storage/store.go | 28 +++ pkg/code/data/cvm/storage/tests/tests.go | 91 ++++++++++ pkg/code/data/internal.go | 50 ++++-- .../cvm/instructions_vm_storage_init.go | 1 - 24 files changed, 1003 insertions(+), 44 deletions(-) create mode 100644 pkg/code/async/vm/service.go create mode 100644 pkg/code/async/vm/storage.go rename pkg/code/data/{vm => cvm}/ram/account.go (100%) rename pkg/code/data/{vm => cvm}/ram/memory/store.go (90%) rename pkg/code/data/{vm => cvm}/ram/memory/store_test.go (74%) rename pkg/code/data/{vm => cvm}/ram/postgres/model.go (98%) rename pkg/code/data/{vm => cvm}/ram/postgres/store.go (74%) rename pkg/code/data/{vm => cvm}/ram/postgres/store_test.go (95%) rename pkg/code/data/{vm => cvm}/ram/store.go (100%) rename pkg/code/data/{vm => cvm}/ram/tests/tests.go (98%) rename pkg/code/data/{vm => cvm}/ram/util.go (100%) rename pkg/code/data/{vm => cvm}/ram/util_test.go (100%) create mode 100644 pkg/code/data/cvm/storage/account.go create mode 100644 pkg/code/data/cvm/storage/memory/store.go create mode 100644 pkg/code/data/cvm/storage/memory/store_test.go create mode 100644 pkg/code/data/cvm/storage/postgres/model.go create mode 100644 pkg/code/data/cvm/storage/postgres/store.go create mode 100644 pkg/code/data/cvm/storage/postgres/store_test.go create mode 100644 pkg/code/data/cvm/storage/store.go create mode 100644 pkg/code/data/cvm/storage/tests/tests.go diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 244d2bbb..22a00e38 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -14,6 +14,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/commitment" + "github.com/code-payments/code-server/pkg/code/data/cvm/storage" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/code/data/transaction" @@ -150,7 +151,7 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransact return nil, err } - memory, accountIndex, err := reserveVmMemory(ctx, h.data, vm.PublicKey().ToBase58(), cvm.VirtualAccountTypeTimelock, fulfillmentRecord.Source) + memory, accountIndex, err := reserveVmMemory(ctx, h.data, vm, cvm.VirtualAccountTypeTimelock, timelockAccounts.Vault) if err != nil { return nil, err } @@ -717,7 +718,7 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c if err != nil { return nil, err } - destinationTimelockOwner, err := common.NewAccountFromPrivateKeyString(timelockRecord.VaultOwner) + destinationTimelockOwner, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) if err != nil { return nil, err } @@ -757,7 +758,7 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, err } - relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, vm.PublicKey().ToBase58(), cvm.VirtualAccountTypeRelay, commitment.PublicKey().ToBase58()) + relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, vm, cvm.VirtualAccountTypeRelay, commitment) if err != nil { return nil, err } @@ -872,14 +873,26 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) SupportsOnDemandTransactio } func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - var vm *common.Account // todo: configure vm account - var storage *common.Account // todo: configure storage account + var vm *common.Account // todo: configure vm account if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { return nil, errors.New("invalid fulfillment type") } - virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vm, nil) + timelockVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) + if err != nil { + return nil, err + } + timelockRecord, err := h.data.GetTimelockByVault(ctx, timelockVault.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + timelockOwner, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) + if err != nil { + return nil, err + } + + virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vm, timelockOwner) if err != nil { return nil, err } @@ -888,6 +901,11 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ct return nil, errors.New("stale timelock account state") } + storage, err := reserveVmStorage(ctx, h.data, vm, storage.PurposeDeletion, timelockVault) + if err != nil { + return nil, err + } + txn, err := transaction_util.MakeCompressAccountTransaction( selectedNonce.Account, selectedNonce.Blockhash, @@ -1037,8 +1055,7 @@ func (h *CloseCommitmentFulfillmentHandler) SupportsOnDemandTransactions() bool } func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - var vm *common.Account // todo: configure vm account - var storage *common.Account // todo: configure storage account + var vm *common.Account // todo: configure vm account if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { return nil, errors.New("invalid fulfillment type") @@ -1054,6 +1071,11 @@ func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context. return nil, err } + storage, err := reserveVmStorage(ctx, h.data, vm, storage.PurposeDeletion, relay) + if err != nil { + return nil, err + } + txn, err := transaction_util.MakeCompressAccountTransaction( selectedNonce.Account, selectedNonce.Blockhash, diff --git a/pkg/code/async/sequencer/vm.go b/pkg/code/async/sequencer/vm.go index 82e5203d..9fd849da 100644 --- a/pkg/code/async/sequencer/vm.go +++ b/pkg/code/async/sequencer/vm.go @@ -10,24 +10,26 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/code/data/cvm/ram" + "github.com/code-payments/code-server/pkg/code/data/cvm/storage" "github.com/code-payments/code-server/pkg/solana/cvm" ) // todo: some of these utilities likely belong in a more common package var ( - // Global VM memory lock + // Global VM storage and memory locks // // todo: Use a distributed lock - vmMemoryLock sync.Mutex + vmMemoryLock sync.Mutex + vmStorageLock sync.Mutex ) -func reserveVmMemory(ctx context.Context, data code_data.Provider, vm string, accountType cvm.VirtualAccountType, address string) (*common.Account, uint16, error) { +func reserveVmMemory(ctx context.Context, data code_data.Provider, vm *common.Account, accountType cvm.VirtualAccountType, account *common.Account) (*common.Account, uint16, error) { vmMemoryLock.Lock() defer vmMemoryLock.Unlock() - memoryAccountAddress, index, err := data.ReserveVmMemory(ctx, vm, accountType, address) + memoryAccountAddress, index, err := data.ReserveVmMemory(ctx, vm.PublicKey().ToBase58(), accountType, account.PublicKey().ToBase58()) if err != nil { return nil, 0, err } @@ -40,6 +42,23 @@ func reserveVmMemory(ctx context.Context, data code_data.Provider, vm string, ac return memoryAccount, index, nil } +func reserveVmStorage(ctx context.Context, data code_data.Provider, vm *common.Account, purpose storage.Purpose, account *common.Account) (*common.Account, error) { + vmStorageLock.Lock() + defer vmStorageLock.Unlock() + + storageAccountAddress, err := data.ReserveVmStorage(ctx, vm.PublicKey().ToBase58(), purpose, account.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + + storageAccount, err := common.NewAccountFromPublicKeyString(storageAccountAddress) + if err != nil { + return nil, err + } + + return storageAccount, nil +} + // This method can be safely called multiple times, since we know "deleted" accounts // will never be reopened or uncompressed back into memory func onVirtualAccountDeleted(ctx context.Context, data code_data.Provider, address string) error { diff --git a/pkg/code/async/vm/service.go b/pkg/code/async/vm/service.go new file mode 100644 index 00000000..3505beef --- /dev/null +++ b/pkg/code/async/vm/service.go @@ -0,0 +1,37 @@ +package async_vm + +import ( + "context" + "time" + + "github.com/sirupsen/logrus" + + "github.com/code-payments/code-server/pkg/code/async" + code_data "github.com/code-payments/code-server/pkg/code/data" +) + +type service struct { + log *logrus.Entry + data code_data.Provider +} + +func New(data code_data.Provider) async.Service { + return &service{ + log: logrus.StandardLogger().WithField("service", "vm"), + data: data, + } +} + +func (p *service) Start(ctx context.Context, interval time.Duration) error { + go func() { + err := p.storageInitWorker(ctx, interval) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("storage init processing loop terminated unexpectedly") + } + }() + + select { + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/pkg/code/async/vm/storage.go b/pkg/code/async/vm/storage.go new file mode 100644 index 00000000..08cedd8d --- /dev/null +++ b/pkg/code/async/vm/storage.go @@ -0,0 +1,144 @@ +package async_vm + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/mr-tron/base58" + "github.com/newrelic/go-agent/v3/newrelic" + + "github.com/code-payments/code-server/pkg/code/common" + "github.com/code-payments/code-server/pkg/code/data/cvm/storage" + "github.com/code-payments/code-server/pkg/metrics" + "github.com/code-payments/code-server/pkg/retry" + "github.com/code-payments/code-server/pkg/solana" + compute_budget "github.com/code-payments/code-server/pkg/solana/computebudget" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +const ( + // todo: make these configurable + maxStorageAccountLevels = 20 // maximum for decompression under a single tx + minStorageAccountCapacity = 50_000 // mimumum capacity for a storage until we decide we need a new one +) + +func (p *service) storageInitWorker(serviceCtx context.Context, interval time.Duration) error { + log := p.log.WithField("method", "storageInitWorker") + + delay := interval + + err := retry.Loop( + func() (err error) { + time.Sleep(delay) + + nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) + m := nr.StartTransaction("async__vm_service__handle_init_storage_account") + defer m.End() + tracedCtx := newrelic.NewContext(serviceCtx, m) + + err = p.maybeInitStorageAccount(tracedCtx) + if err != nil { + m.NoticeError(err) + log.WithError(err).Warn("failure handling init storage account") + } + return err + }, + retry.NonRetriableErrors(context.Canceled), + ) + + return err +} + +func (p *service) maybeInitStorageAccount(ctx context.Context) error { + var vm *common.Account // todo: configure vm account + + // todo: iterate over purposes when we have more than one + purpose := storage.PurposeDeletion + + _, err := p.data.FindAnyVmStorageWithAvailableCapacity(ctx, vm.PublicKey().ToBase58(), purpose, minStorageAccountCapacity) + switch err { + case storage.ErrNotFound: + case nil: + return nil + default: + return err + } + + record, err := p.initStorageAccountOnBlockchain(ctx, vm, purpose) + if err != nil { + return err + } + + return p.data.InitializeVmStorage(ctx, record) +} + +func (p *service) initStorageAccountOnBlockchain(ctx context.Context, vm *common.Account, purpose storage.Purpose) (*storage.Record, error) { + name := fmt.Sprintf("storage-%d-%s", purpose, strings.Split(uuid.New().String(), "-")[0]) + levels := uint8(maxStorageAccountLevels) + + address, _, err := cvm.GetStorageAccountAddress(&cvm.GetMemoryAccountAddressArgs{ + Name: name, + Vm: vm.PublicKey().ToBytes(), + }) + if err != nil { + return nil, err + } + + record := &storage.Record{ + Vm: vm.PublicKey().ToBase58(), + + Name: name, + Address: base58.Encode(address), + Levels: levels, + AvailableCapacity: storage.GetMaxCapacity(levels), + Purpose: purpose, + } + + txn := solana.NewTransaction( + common.GetSubsidizer().PublicKey().ToBytes(), + compute_budget.SetComputeUnitLimit(10_000), + compute_budget.SetComputeUnitPrice(10_000), + cvm.NewVmStorageInitInstruction( + &cvm.VmStorageInitInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmStorage: address, + }, + &cvm.VmStorageInitInstructionArgs{ + Name: name, + Levels: levels, + }, + ), + ) + + bh, err := p.data.GetBlockchainLatestBlockhash(ctx) + if err != nil { + return nil, err + } + txn.SetBlockhash(bh) + + err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) + if err != nil { + return nil, err + } + + for i := 0; i < 30; i++ { + sig, err := p.data.SubmitBlockchainTransaction(ctx, &txn) + if err != nil { + return nil, err + } + + time.Sleep(4 * time.Second) + + _, err = p.data.GetBlockchainTransaction(ctx, base58.Encode(sig[:]), solana.CommitmentFinalized) + if err == nil { + return record, nil + } + } + + return nil, errors.New("txn did not finalize") +} diff --git a/pkg/code/data/vm/ram/account.go b/pkg/code/data/cvm/ram/account.go similarity index 100% rename from pkg/code/data/vm/ram/account.go rename to pkg/code/data/cvm/ram/account.go diff --git a/pkg/code/data/vm/ram/memory/store.go b/pkg/code/data/cvm/ram/memory/store.go similarity index 90% rename from pkg/code/data/vm/ram/memory/store.go rename to pkg/code/data/cvm/ram/memory/store.go index f0c17bc0..807152ba 100644 --- a/pkg/code/data/vm/ram/memory/store.go +++ b/pkg/code/data/cvm/ram/memory/store.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/code/data/cvm/ram" "github.com/code-payments/code-server/pkg/solana/cvm" ) @@ -18,7 +18,7 @@ type store struct { storedVirtualAccounts map[string]string } -// New returns a new in memory vm.ram.Store +// New returns a new in memory cvm.ram.Store func New() ram.Store { return &store{ reservedAccountIndices: make(map[string]string), @@ -26,7 +26,7 @@ func New() ram.Store { } } -// InitializeMemory implements vm.ram.Store.InitializeMemory +// InitializeMemory implements cvm.ram.Store.InitializeMemory func (s *store) InitializeMemory(_ context.Context, record *ram.Record) error { if err := record.Validate(); err != nil { return err @@ -51,7 +51,7 @@ func (s *store) InitializeMemory(_ context.Context, record *ram.Record) error { return nil } -// FreeMemoryByIndex implements vm.ram.Store.FreeMemoryByIndex +// FreeMemoryByIndex implements cvm.ram.Store.FreeMemoryByIndex func (s *store) FreeMemoryByIndex(_ context.Context, memoryAccount string, index uint16) error { s.mu.Lock() defer s.mu.Unlock() @@ -68,7 +68,7 @@ func (s *store) FreeMemoryByIndex(_ context.Context, memoryAccount string, index return nil } -// FreeMemoryByAddress implements vm.ram.Store.FreeMemoryByAddress +// FreeMemoryByAddress implements cvm.ram.Store.FreeMemoryByAddress func (s *store) FreeMemoryByAddress(_ context.Context, address string) error { s.mu.Lock() defer s.mu.Unlock() @@ -84,7 +84,7 @@ func (s *store) FreeMemoryByAddress(_ context.Context, address string) error { return nil } -// ReserveMemory implements vm.ram.Store.ReserveMemory +// ReserveMemory implements cvm.ram.Store.ReserveMemory func (s *store) ReserveMemory(_ context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) { s.mu.Lock() defer s.mu.Unlock() diff --git a/pkg/code/data/vm/ram/memory/store_test.go b/pkg/code/data/cvm/ram/memory/store_test.go similarity index 74% rename from pkg/code/data/vm/ram/memory/store_test.go rename to pkg/code/data/cvm/ram/memory/store_test.go index 33e24fd2..817eba64 100644 --- a/pkg/code/data/vm/ram/memory/store_test.go +++ b/pkg/code/data/cvm/ram/memory/store_test.go @@ -3,7 +3,7 @@ package memory import ( "testing" - "github.com/code-payments/code-server/pkg/code/data/vm/ram/tests" + "github.com/code-payments/code-server/pkg/code/data/cvm/ram/tests" ) func TestVmRamMemoryStore(t *testing.T) { diff --git a/pkg/code/data/vm/ram/postgres/model.go b/pkg/code/data/cvm/ram/postgres/model.go similarity index 98% rename from pkg/code/data/vm/ram/postgres/model.go rename to pkg/code/data/cvm/ram/postgres/model.go index 7209e407..edd06d2f 100644 --- a/pkg/code/data/vm/ram/postgres/model.go +++ b/pkg/code/data/cvm/ram/postgres/model.go @@ -7,7 +7,7 @@ import ( "github.com/jmoiron/sqlx" - "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/code/data/cvm/ram" pgutil "github.com/code-payments/code-server/pkg/database/postgres" "github.com/code-payments/code-server/pkg/solana/cvm" ) diff --git a/pkg/code/data/vm/ram/postgres/store.go b/pkg/code/data/cvm/ram/postgres/store.go similarity index 74% rename from pkg/code/data/vm/ram/postgres/store.go rename to pkg/code/data/cvm/ram/postgres/store.go index b4e847a7..6b0eaa89 100644 --- a/pkg/code/data/vm/ram/postgres/store.go +++ b/pkg/code/data/cvm/ram/postgres/store.go @@ -6,7 +6,7 @@ import ( "github.com/jmoiron/sqlx" - "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/code/data/cvm/ram" "github.com/code-payments/code-server/pkg/solana/cvm" ) @@ -14,14 +14,14 @@ type store struct { db *sqlx.DB } -// New returns a new postgres vm.ram.Store +// New returns a new postgres cvm.ram.Store func New(db *sql.DB) ram.Store { return &store{ db: sqlx.NewDb(db, "pgx"), } } -// InitializeMemory implements vm.ram.Store.InitializeMemory +// InitializeMemory implements cvm.ram.Store.InitializeMemory func (s *store) InitializeMemory(ctx context.Context, record *ram.Record) error { model, err := toAccountModel(record) if err != nil { @@ -39,17 +39,17 @@ func (s *store) InitializeMemory(ctx context.Context, record *ram.Record) error return nil } -// FreeMemoryByIndex implements vm.ram.Store.FreeMemoryByIndex +// FreeMemoryByIndex implements cvm.ram.Store.FreeMemoryByIndex func (s *store) FreeMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error { return dbFreeMemoryByIndex(ctx, s.db, memoryAccount, index) } -// FreeMemoryByAddress implements vm.ram.Store.FreeMemoryByAddress +// FreeMemoryByAddress implements cvm.ram.Store.FreeMemoryByAddress func (s *store) FreeMemoryByAddress(ctx context.Context, address string) error { return dbFreeMemoryByAddress(ctx, s.db, address) } -// ReserveMemory implements vm.ram.Store.ReserveMemory +// ReserveMemory implements cvm.ram.Store.ReserveMemory func (s *store) ReserveMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) { return dbReserveMemory(ctx, s.db, vm, accountType, address) } diff --git a/pkg/code/data/vm/ram/postgres/store_test.go b/pkg/code/data/cvm/ram/postgres/store_test.go similarity index 95% rename from pkg/code/data/vm/ram/postgres/store_test.go rename to pkg/code/data/cvm/ram/postgres/store_test.go index 1359fff4..74c60dd9 100644 --- a/pkg/code/data/vm/ram/postgres/store_test.go +++ b/pkg/code/data/cvm/ram/postgres/store_test.go @@ -8,8 +8,8 @@ import ( "github.com/ory/dockertest/v3" "github.com/sirupsen/logrus" - "github.com/code-payments/code-server/pkg/code/data/vm/ram" - "github.com/code-payments/code-server/pkg/code/data/vm/ram/tests" + "github.com/code-payments/code-server/pkg/code/data/cvm/ram" + "github.com/code-payments/code-server/pkg/code/data/cvm/ram/tests" postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" diff --git a/pkg/code/data/vm/ram/store.go b/pkg/code/data/cvm/ram/store.go similarity index 100% rename from pkg/code/data/vm/ram/store.go rename to pkg/code/data/cvm/ram/store.go diff --git a/pkg/code/data/vm/ram/tests/tests.go b/pkg/code/data/cvm/ram/tests/tests.go similarity index 98% rename from pkg/code/data/vm/ram/tests/tests.go rename to pkg/code/data/cvm/ram/tests/tests.go index c2bf8458..11b82dd5 100644 --- a/pkg/code/data/vm/ram/tests/tests.go +++ b/pkg/code/data/cvm/ram/tests/tests.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/code/data/vm/ram" + "github.com/code-payments/code-server/pkg/code/data/cvm/ram" "github.com/code-payments/code-server/pkg/solana/cvm" ) diff --git a/pkg/code/data/vm/ram/util.go b/pkg/code/data/cvm/ram/util.go similarity index 100% rename from pkg/code/data/vm/ram/util.go rename to pkg/code/data/cvm/ram/util.go diff --git a/pkg/code/data/vm/ram/util_test.go b/pkg/code/data/cvm/ram/util_test.go similarity index 100% rename from pkg/code/data/vm/ram/util_test.go rename to pkg/code/data/cvm/ram/util_test.go diff --git a/pkg/code/data/cvm/storage/account.go b/pkg/code/data/cvm/storage/account.go new file mode 100644 index 00000000..5ccbe4fa --- /dev/null +++ b/pkg/code/data/cvm/storage/account.go @@ -0,0 +1,90 @@ +package storage + +import ( + "errors" + "math" + "time" +) + +type Purpose uint8 + +const ( + PurposeUnknown Purpose = iota + PurposeDeletion // Purely used to "delete" accounts that will never make it back to memory. At capacity, we can throw the entire account away and any DB storage. +) + +type Record struct { + Id uint64 + + Vm string + + Name string + Address string + Levels uint8 + AvailableCapacity uint64 + Purpose Purpose + + CreatedAt time.Time +} + +func (r *Record) Validate() error { + if len(r.Vm) == 0 { + return errors.New("vm is required") + } + + if len(r.Address) == 0 { + return errors.New("address is required") + } + + if len(r.Name) == 0 { + return errors.New("name is required") + } + + if r.Levels == 0 { + return errors.New("levels is required") + } + + if r.AvailableCapacity > GetMaxCapacity(r.Levels) { + return errors.New("available capacity exceeds maximum") + } + + if r.Purpose != PurposeDeletion { + return errors.New("invalid purpose") + } + + return nil +} + +func (r *Record) Clone() Record { + return Record{ + Id: r.Id, + + Vm: r.Vm, + + Name: r.Name, + Address: r.Address, + Levels: r.Levels, + AvailableCapacity: r.AvailableCapacity, + Purpose: r.Purpose, + + CreatedAt: r.CreatedAt, + } +} + +func (r *Record) CopyTo(dst *Record) { + dst.Id = r.Id + + dst.Vm = r.Vm + + dst.Name = r.Name + dst.Address = r.Address + dst.Levels = r.Levels + dst.AvailableCapacity = r.AvailableCapacity + dst.Purpose = r.Purpose + + dst.CreatedAt = r.CreatedAt +} + +func GetMaxCapacity(levels uint8) uint64 { + return uint64(math.Pow(2, float64(levels))) - 1 +} diff --git a/pkg/code/data/cvm/storage/memory/store.go b/pkg/code/data/cvm/storage/memory/store.go new file mode 100644 index 00000000..0dddb9b5 --- /dev/null +++ b/pkg/code/data/cvm/storage/memory/store.go @@ -0,0 +1,138 @@ +package memory + +import ( + "context" + "sync" + "time" + + "github.com/code-payments/code-server/pkg/code/data/cvm/storage" +) + +type store struct { + mu sync.Mutex + last uint64 + records []*storage.Record + storedAccounts map[string]struct{} +} + +// New returns a new in memory cvm.storage.Store +func New() storage.Store { + return &store{ + storedAccounts: make(map[string]struct{}), + } +} + +// InitializeStorage implements cvm.storage.Store.InitializeStorage +func (s *store) InitializeStorage(_ context.Context, record *storage.Record) error { + if err := record.Validate(); err != nil { + return err + } + + if record.AvailableCapacity != storage.GetMaxCapacity(record.Levels) { + return storage.ErrInvalidInitialCapacity + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.last++ + if item := s.find(record); item != nil { + return storage.ErrAlreadyInitialized + } + + record.Id = s.last + if record.CreatedAt.IsZero() { + record.CreatedAt = time.Now() + } + + cloned := record.Clone() + s.records = append(s.records, &cloned) + + return nil +} + +// FindAnyWithAvailableCapacity implements cvm.storage.Store.FindAnyWithAvailableCapacity +func (s *store) FindAnyWithAvailableCapacity(_ context.Context, vm string, purpose storage.Purpose, minCapacity uint64) (*storage.Record, error) { + s.mu.Lock() + defer s.mu.Unlock() + + items := s.findByVmAndPurpose(vm, purpose) + items = s.filterByAvailableStorage(items, minCapacity) + + if len(items) == 0 { + return nil, storage.ErrNotFound + } + + cloned := items[0].Clone() + return &cloned, nil +} + +// ReserveStorage implements cvm.storage.Store.ReserveStorage +func (s *store) ReserveStorage(_ context.Context, vm string, purpose storage.Purpose, address string) (string, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.storedAccounts[address]; ok { + return "", storage.ErrAddressAlreadyReserved + } + + items := s.findByVmAndPurpose(vm, purpose) + items = s.filterByAvailableStorage(items, 1) + + if len(items) == 0 { + return "", storage.ErrNoFreeStorage + } + + s.storedAccounts[address] = struct{}{} + selected := items[0] + selected.AvailableCapacity -= 1 + return selected.Address, nil +} + +func (s *store) find(data *storage.Record) *storage.Record { + for _, item := range s.records { + if item.Id == data.Id { + return item + } + + if item.Address == data.Address { + return item + } + } + + return nil +} + +func (s *store) findByVmAndPurpose(vm string, purpose storage.Purpose) []*storage.Record { + var res []*storage.Record + for _, item := range s.records { + if item.Vm != vm { + continue + } + + if item.Purpose != purpose { + continue + } + + res = append(res, item) + } + return res +} + +func (s *store) filterByAvailableStorage(items []*storage.Record, minCapacity uint64) []*storage.Record { + var res []*storage.Record + for _, item := range items { + if item.AvailableCapacity >= minCapacity { + res = append(res, item) + } + } + return res +} + +func (s *store) reset() { + s.mu.Lock() + defer s.mu.Unlock() + s.last = 0 + s.records = nil + s.storedAccounts = make(map[string]struct{}) +} diff --git a/pkg/code/data/cvm/storage/memory/store_test.go b/pkg/code/data/cvm/storage/memory/store_test.go new file mode 100644 index 00000000..299e9f0d --- /dev/null +++ b/pkg/code/data/cvm/storage/memory/store_test.go @@ -0,0 +1,15 @@ +package memory + +import ( + "testing" + + "github.com/code-payments/code-server/pkg/code/data/cvm/storage/tests" +) + +func TestVmStorageMemoryStore(t *testing.T) { + testStore := New() + teardown := func() { + testStore.(*store).reset() + } + tests.RunTests(t, testStore, teardown) +} diff --git a/pkg/code/data/cvm/storage/postgres/model.go b/pkg/code/data/cvm/storage/postgres/model.go new file mode 100644 index 00000000..ebc7fb9b --- /dev/null +++ b/pkg/code/data/cvm/storage/postgres/model.go @@ -0,0 +1,169 @@ +package postgres + +import ( + "context" + "database/sql" + "time" + + "github.com/jmoiron/sqlx" + + "github.com/code-payments/code-server/pkg/code/data/cvm/storage" + pgutil "github.com/code-payments/code-server/pkg/database/postgres" +) + +const ( + accountTableName = "codewallet__core_vmstorageaccount" + allocatedStorageTableName = "codewallet__core_vmstorageallocatedstorage" +) + +type accountModel struct { + Id sql.NullInt64 `db:"id"` + + Vm string `db:"vm"` + + Name string `db:"name"` + Address string `db:"address"` + Levels uint8 `db:"levels"` + AvailableCapacity uint64 `db:"available_capacity"` + Purpose uint8 `db:"purpose"` + + CreatedAt time.Time `db:"created_at"` +} + +type allocatedStorageModel struct { + Id sql.NullInt64 `db:"id"` + + Vm string `db:"vm"` + + StorageAccount string `db:"storage_account"` + Address string `db:"address"` + + CreatedAt time.Time `db:"created_at"` +} + +func toAccountModel(obj *storage.Record) (*accountModel, error) { + if err := obj.Validate(); err != nil { + return nil, err + } + + return &accountModel{ + Vm: obj.Vm, + + Name: obj.Name, + Address: obj.Address, + Levels: obj.Levels, + AvailableCapacity: obj.AvailableCapacity, + Purpose: uint8(obj.Purpose), + + CreatedAt: obj.CreatedAt, + }, nil +} + +func fromAccountModel(obj *accountModel) *storage.Record { + return &storage.Record{ + Id: uint64(obj.Id.Int64), + + Vm: obj.Vm, + + Name: obj.Name, + Address: obj.Address, + Levels: obj.Levels, + AvailableCapacity: obj.AvailableCapacity, + Purpose: storage.Purpose(obj.Purpose), + + CreatedAt: obj.CreatedAt, + } +} + +func (m *accountModel) dbInitialize(ctx context.Context, db *sqlx.DB) error { + return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { + query := `INSERT INTO ` + accountTableName + ` + (vm, name, address, levels, available_capacity, purpose, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, vm, name, address, levels, available_capacity, purpose, created_at` + + if m.CreatedAt.IsZero() { + m.CreatedAt = time.Now() + } + + err := tx.QueryRowxContext( + ctx, + query, + m.Vm, + m.Name, + m.Address, + m.Levels, + m.AvailableCapacity, + m.Purpose, + m.CreatedAt, + ).StructScan(m) + + return pgutil.CheckUniqueViolation(err, storage.ErrAlreadyInitialized) + }) +} + +func dbFindAnyWithAvailableCapacity(ctx context.Context, db *sqlx.DB, vm string, purpose storage.Purpose, minCapacity uint64) (*accountModel, error) { + res := &accountModel{} + + query := `SELECT + id, vm, name, address, levels, available_capacity, purpose, created_at + FROM ` + accountTableName + ` + WHERE vm = $1 AND purpose = $2 AND available_capacity >= $3 + LIMIT 1` + + err := db.GetContext( + ctx, + res, + query, + vm, + purpose, + minCapacity, + ) + if err != nil { + return nil, pgutil.CheckNoRows(err, storage.ErrNotFound) + } + return res, nil +} + +func dbReserveStorage(ctx context.Context, db *sqlx.DB, vm string, purpose storage.Purpose, address string) (string, error) { + var storageAccount string + err := pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { + var model accountModel + + query1 := `INSERT INTO ` + allocatedStorageTableName + ` + (vm, storage_account, address, created_at) + VALUES ($1, $2, $3, $4) + RETURNING id, vm, storage_account, address, created_at` + err := tx.QueryRowxContext( + ctx, + query1, + vm, + model.Address, + address, + time.Now(), + ).StructScan(&allocatedStorageModel{}) + if err != nil { + return pgutil.CheckUniqueViolation(err, storage.ErrAddressAlreadyReserved) + } + + query2 := `UPDATE ` + accountTableName + ` + SET available_capacity = available_capacity - 1 + WHERE vm = $1 AND purpose = $2 and available_capacity > 0 + RETURNING id, vm, name, address, levels, available_capacity, purpose, created_at` + + err = tx.QueryRowxContext( + ctx, + query2, + vm, + purpose, + ).StructScan(&model) + if err != nil { + return pgutil.CheckNoRows(err, storage.ErrNoFreeStorage) + } + + storageAccount = model.Address + + return nil + }) + return storageAccount, err +} diff --git a/pkg/code/data/cvm/storage/postgres/store.go b/pkg/code/data/cvm/storage/postgres/store.go new file mode 100644 index 00000000..06f3cb21 --- /dev/null +++ b/pkg/code/data/cvm/storage/postgres/store.go @@ -0,0 +1,56 @@ +package postgres + +import ( + "context" + "database/sql" + + "github.com/jmoiron/sqlx" + + "github.com/code-payments/code-server/pkg/code/data/cvm/storage" +) + +type store struct { + db *sqlx.DB +} + +// New returns a new postgres vm.storage.Store +func New(db *sql.DB) storage.Store { + return &store{ + db: sqlx.NewDb(db, "pgx"), + } +} + +// InitializeStorage implements vm.storage.Store.InitializeStorage +func (s *store) InitializeStorage(ctx context.Context, record *storage.Record) error { + if record.AvailableCapacity != storage.GetMaxCapacity(record.Levels) { + return storage.ErrInvalidInitialCapacity + } + + model, err := toAccountModel(record) + if err != nil { + return err + } + + err = model.dbInitialize(ctx, s.db) + if err != nil { + return err + } + + fromAccountModel(model).CopyTo(record) + + return nil +} + +// FindAnyWithAvailableCapacity implements cvm.storage.Store.FindAnyWithAvailableCapacity +func (s *store) FindAnyWithAvailableCapacity(ctx context.Context, vm string, purpose storage.Purpose, minCapacity uint64) (*storage.Record, error) { + model, err := dbFindAnyWithAvailableCapacity(ctx, s.db, vm, purpose, minCapacity) + if err != nil { + return nil, err + } + return fromAccountModel(model), nil +} + +// ReserveStorage implements cvm.storage.Store.ReserveStorage +func (s *store) ReserveStorage(ctx context.Context, vm string, purpose storage.Purpose, address string) (string, error) { + return dbReserveStorage(ctx, s.db, vm, purpose, address) +} diff --git a/pkg/code/data/cvm/storage/postgres/store_test.go b/pkg/code/data/cvm/storage/postgres/store_test.go new file mode 100644 index 00000000..a469212a --- /dev/null +++ b/pkg/code/data/cvm/storage/postgres/store_test.go @@ -0,0 +1,127 @@ +package postgres + +import ( + "database/sql" + "os" + "testing" + + "github.com/ory/dockertest/v3" + "github.com/sirupsen/logrus" + + "github.com/code-payments/code-server/pkg/code/data/cvm/storage" + "github.com/code-payments/code-server/pkg/code/data/cvm/storage/tests" + + postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" + + _ "github.com/jackc/pgx/v4/stdlib" +) + +var ( + testStore storage.Store + teardown func() +) + +const ( + // Used for testing ONLY, the table and migrations are external to this repository + tableCreate = ` + CREATE TABLE codewallet__core_vmstorageaccount ( + id SERIAL NOT NULL PRIMARY KEY, + + vm TEXT NOT NULL, + + name TEXT NOT NULL, + address TEXT NOT NULL, + levels INTEGER NOT NULL, + available_capacity BIGINT NOT NULL CHECK(available_capacity >= 0), + purpose INTEGER NOT NULL, + + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + + CONSTRAINT codewallet__core_vmmemoryaccount__uniq__address UNIQUE (address) + ); + + CREATE TABLE codewallet__core_vmstorageallocatedstorage ( + id SERIAL NOT NULL PRIMARY KEY, + + vm TEXT NOT NULL, + + storage_account TEXT NOT NULL, + address TEXT NULL, + + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + + CONSTRAINT codewallet__core_vmmemoryallocatedmemory__uniq__address UNIQUE (address) + ); + ` + + // Used for testing ONLY, the table and migrations are external to this repository + tableDestroy = ` + DROP TABLE codewallet__core_vmstorageaccount; + DROP TABLE codewallet__core_vmstorageallocatedstorage; + ` +) + +func TestMain(m *testing.M) { + log := logrus.StandardLogger() + + testPool, err := dockertest.NewPool("") + if err != nil { + log.WithError(err).Error("Error creating docker pool") + os.Exit(1) + } + + var cleanUpFunc func() + db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) + if err != nil { + log.WithError(err).Error("Error starting postgres image") + os.Exit(1) + } + defer db.Close() + + if err := createTestTables(db); err != nil { + logrus.StandardLogger().WithError(err).Error("Error creating test tables") + cleanUpFunc() + os.Exit(1) + } + + testStore = New(db) + teardown = func() { + if pc := recover(); pc != nil { + cleanUpFunc() + panic(pc) + } + + if err := resetTestTables(db); err != nil { + logrus.StandardLogger().WithError(err).Error("Error resetting test tables") + cleanUpFunc() + os.Exit(1) + } + } + + code := m.Run() + cleanUpFunc() + os.Exit(code) +} + +func TestVmStoragePostgresStore(t *testing.T) { + tests.RunTests(t, testStore, teardown) +} + +func createTestTables(db *sql.DB) error { + _, err := db.Exec(tableCreate) + if err != nil { + logrus.StandardLogger().WithError(err).Error("could not create test tables") + return err + } + return nil +} + +func resetTestTables(db *sql.DB) error { + _, err := db.Exec(tableDestroy) + if err != nil { + logrus.StandardLogger().WithError(err).Error("could not drop test tables") + return err + } + + return createTestTables(db) +} diff --git a/pkg/code/data/cvm/storage/store.go b/pkg/code/data/cvm/storage/store.go new file mode 100644 index 00000000..b8188b90 --- /dev/null +++ b/pkg/code/data/cvm/storage/store.go @@ -0,0 +1,28 @@ +package storage + +import ( + "context" + "errors" +) + +var ( + ErrAddressAlreadyReserved = errors.New("virtual account address already in storage") + ErrAlreadyInitialized = errors.New("storage account already initalized") + ErrInvalidInitialCapacity = errors.New("available capacity must be maximum when initializing storage") + ErrNoFreeStorage = errors.New("no available free storage") + ErrNotFound = errors.New("no storage accounts found") +) + +// Store implements a basic construct for managing compression storage. +// +// Note: A lock outside this implementation is required to resolve any races. +type Store interface { + // Initializes a VM storage account for management + InitializeStorage(ctx context.Context, record *Record) error + + // FindAnyWithAvailableCapacity finds a VM storage account with minimum available capcity + FindAnyWithAvailableCapacity(ctx context.Context, vm string, purpose Purpose, minCapacity uint64) (*Record, error) + + // ReserveStorage reserves a piece of storage in a VM for the virtual account address + ReserveStorage(ctx context.Context, vm string, purpose Purpose, address string) (string, error) +} diff --git a/pkg/code/data/cvm/storage/tests/tests.go b/pkg/code/data/cvm/storage/tests/tests.go new file mode 100644 index 00000000..d9545c81 --- /dev/null +++ b/pkg/code/data/cvm/storage/tests/tests.go @@ -0,0 +1,91 @@ +package tests + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/code-payments/code-server/pkg/code/data/cvm/storage" +) + +func RunTests(t *testing.T, s storage.Store, teardown func()) { + for _, tf := range []func(t *testing.T, s storage.Store){ + testHappyPath, + } { + tf(t, s) + teardown() + } +} + +func testHappyPath(t *testing.T, s storage.Store) { + t.Run("testHappyPath", func(t *testing.T) { + ctx := context.Background() + + record := &storage.Record{ + Vm: "vm1", + Name: "name1", + Address: "storageaccount1", + Levels: 4, + AvailableCapacity: storage.GetMaxCapacity(4) - 1, + Purpose: storage.PurposeDeletion, + } + + assert.Equal(t, storage.ErrInvalidInitialCapacity, s.InitializeStorage(ctx, record)) + + record.AvailableCapacity = storage.GetMaxCapacity(record.Levels) + cloned := record.Clone() + + start := time.Now() + require.NoError(t, s.InitializeStorage(ctx, record)) + assert.True(t, record.Id > 0) + assert.True(t, record.CreatedAt.After(start)) + + assert.Equal(t, storage.ErrAlreadyInitialized, s.InitializeStorage(ctx, record)) + + actual, err := s.FindAnyWithAvailableCapacity(ctx, "vm1", storage.PurposeDeletion, 1) + require.NoError(t, err) + assertEquivalentRecords(t, &cloned, actual) + + _, err = s.FindAnyWithAvailableCapacity(ctx, "vm1", storage.PurposeDeletion, storage.GetMaxCapacity(record.Levels)+1) + assert.Equal(t, storage.ErrNotFound, err) + + _, err = s.FindAnyWithAvailableCapacity(ctx, "vm1", storage.PurposeUnknown, 1) + assert.Equal(t, storage.ErrNotFound, err) + + _, err = s.FindAnyWithAvailableCapacity(ctx, "vm2", storage.PurposeDeletion, 1) + assert.Equal(t, storage.ErrNotFound, err) + + _, err = s.ReserveStorage(ctx, "vm2", storage.PurposeDeletion, "virtualaccount") + assert.Equal(t, storage.ErrNoFreeStorage, err) + + _, err = s.ReserveStorage(ctx, "vm1", storage.PurposeUnknown, "virtualaccount") + assert.Equal(t, storage.ErrNoFreeStorage, err) + + for i := 0; i < int(storage.GetMaxCapacity(4)); i++ { + virtualAccountAddress := fmt.Sprintf("virtualaccount%d", i) + + storageAccount, err := s.ReserveStorage(ctx, "vm1", storage.PurposeDeletion, virtualAccountAddress) + require.NoError(t, err) + assert.Equal(t, "storageaccount1", storageAccount) + + _, err = s.ReserveStorage(ctx, "vm1", storage.PurposeDeletion, virtualAccountAddress) + assert.Equal(t, storage.ErrAddressAlreadyReserved, err) + } + + _, err = s.ReserveStorage(ctx, "vm1", storage.PurposeDeletion, "newvirtualaccount") + assert.Equal(t, storage.ErrNoFreeStorage, err) + }) +} + +func assertEquivalentRecords(t *testing.T, obj1, obj2 *storage.Record) { + assert.Equal(t, obj1.Vm, obj2.Vm) + assert.Equal(t, obj1.Name, obj2.Name) + assert.Equal(t, obj1.Address, obj2.Address) + assert.Equal(t, obj1.Levels, obj2.Levels) + assert.Equal(t, obj1.AvailableCapacity, obj2.AvailableCapacity) + assert.Equal(t, obj1.Purpose, obj2.Purpose) +} diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 0781bf25..c49794f3 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -30,6 +30,8 @@ import ( "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/contact" "github.com/code-payments/code-server/pkg/code/data/currency" + cvm_ram "github.com/code-payments/code-server/pkg/code/data/cvm/ram" + cvm_storage "github.com/code-payments/code-server/pkg/code/data/cvm/storage" "github.com/code-payments/code-server/pkg/code/data/deposit" "github.com/code-payments/code-server/pkg/code/data/event" "github.com/code-payments/code-server/pkg/code/data/fulfillment" @@ -53,7 +55,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/user/identity" "github.com/code-payments/code-server/pkg/code/data/user/storage" "github.com/code-payments/code-server/pkg/code/data/vault" - vm_ram "github.com/code-payments/code-server/pkg/code/data/vm/ram" "github.com/code-payments/code-server/pkg/code/data/webhook" account_memory_client "github.com/code-payments/code-server/pkg/code/data/account/memory" @@ -65,6 +66,8 @@ import ( commitment_memory_client "github.com/code-payments/code-server/pkg/code/data/commitment/memory" contact_memory_client "github.com/code-payments/code-server/pkg/code/data/contact/memory" currency_memory_client "github.com/code-payments/code-server/pkg/code/data/currency/memory" + cvm_ram_memory_client "github.com/code-payments/code-server/pkg/code/data/cvm/ram/memory" + cvm_storage_memory_client "github.com/code-payments/code-server/pkg/code/data/cvm/storage/memory" deposit_memory_client "github.com/code-payments/code-server/pkg/code/data/deposit/memory" event_memory_client "github.com/code-payments/code-server/pkg/code/data/event/memory" fulfillment_memory_client "github.com/code-payments/code-server/pkg/code/data/fulfillment/memory" @@ -89,7 +92,6 @@ import ( user_identity_memory_client "github.com/code-payments/code-server/pkg/code/data/user/identity/memory" user_storage_memory_client "github.com/code-payments/code-server/pkg/code/data/user/storage/memory" vault_memory_client "github.com/code-payments/code-server/pkg/code/data/vault/memory" - vm_ram_memory_client "github.com/code-payments/code-server/pkg/code/data/vm/ram/memory" webhook_memory_client "github.com/code-payments/code-server/pkg/code/data/webhook/memory" account_postgres_client "github.com/code-payments/code-server/pkg/code/data/account/postgres" @@ -101,6 +103,8 @@ import ( commitment_postgres_client "github.com/code-payments/code-server/pkg/code/data/commitment/postgres" contact_postgres_client "github.com/code-payments/code-server/pkg/code/data/contact/postgres" currency_postgres_client "github.com/code-payments/code-server/pkg/code/data/currency/postgres" + cvm_ram_postgres_client "github.com/code-payments/code-server/pkg/code/data/cvm/ram/postgres" + cvm_storage_postgres_client "github.com/code-payments/code-server/pkg/code/data/cvm/storage/postgres" deposit_postgres_client "github.com/code-payments/code-server/pkg/code/data/deposit/postgres" event_postgres_client "github.com/code-payments/code-server/pkg/code/data/event/postgres" fulfillment_postgres_client "github.com/code-payments/code-server/pkg/code/data/fulfillment/postgres" @@ -124,7 +128,6 @@ import ( user_identity_postgres_client "github.com/code-payments/code-server/pkg/code/data/user/identity/postgres" user_storage_postgres_client "github.com/code-payments/code-server/pkg/code/data/user/storage/postgres" vault_postgres_client "github.com/code-payments/code-server/pkg/code/data/vault/postgres" - vm_ram_postgres_client "github.com/code-payments/code-server/pkg/code/data/vm/ram/postgres" webhook_postgres_client "github.com/code-payments/code-server/pkg/code/data/webhook/postgres" ) @@ -439,13 +442,19 @@ type DatabaseData interface { IsTweetProcessed(ctx context.Context, tweetId string) (bool, error) MarkTwitterNonceAsUsed(ctx context.Context, tweetId string, nonce uuid.UUID) error - // VM RAM + // CVM RAM // -------------------------------------------------------------------------------- - InitializeVmMemory(ctx context.Context, record *vm_ram.Record) error + InitializeVmMemory(ctx context.Context, record *cvm_ram.Record) error FreeVmMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error FreeVmMemoryByAddress(ctx context.Context, address string) error ReserveVmMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) + // CVM Storage + // -------------------------------------------------------------------------------- + InitializeVmStorage(ctx context.Context, record *cvm_storage.Record) error + FindAnyVmStorageWithAvailableCapacity(ctx context.Context, vm string, purpose cvm_storage.Purpose, minCapacity uint64) (*cvm_storage.Record, error) + ReserveVmStorage(ctx context.Context, vm string, purpose cvm_storage.Purpose, address string) (string, error) + // ExecuteInTx executes fn with a single DB transaction that is scoped to the call. // This enables more complex transactions that can span many calls across the provider. // @@ -489,7 +498,8 @@ type DatabaseProvider struct { preferences preferences.Store airdrop airdrop.Store twitter twitter.Store - vmRam vm_ram.Store + cvmRam cvm_ram.Store + cvmStorage cvm_storage.Store exchangeCache cache.Cache timelockCache cache.Cache @@ -552,7 +562,8 @@ func NewDatabaseProvider(dbConfig *pg.Config) (DatabaseData, error) { preferences: preferences_postgres_client.New(db), airdrop: airdrop_postgres_client.New(db), twitter: twitter_postgres_client.New(db), - vmRam: vm_ram_postgres_client.New(db), + cvmRam: cvm_ram_postgres_client.New(db), + cvmStorage: cvm_storage_postgres_client.New(db), exchangeCache: cache.NewCache(maxExchangeRateCacheBudget), timelockCache: cache.NewCache(maxTimelockCacheBudget), @@ -596,7 +607,8 @@ func NewTestDatabaseProvider() DatabaseData { preferences: preferences_memory_client.New(), airdrop: airdrop_memory_client.New(), twitter: twitter_memory_client.New(), - vmRam: vm_ram_memory_client.New(), + cvmRam: cvm_ram_memory_client.New(), + cvmStorage: cvm_storage_memory_client.New(), exchangeCache: cache.NewCache(maxExchangeRateCacheBudget), timelockCache: nil, // Shouldn't be used for tests @@ -1564,15 +1576,27 @@ func (dp *DatabaseProvider) MarkTwitterNonceAsUsed(ctx context.Context, tweetId // VM RAM // -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) InitializeVmMemory(ctx context.Context, record *vm_ram.Record) error { - return dp.vmRam.InitializeMemory(ctx, record) +func (dp *DatabaseProvider) InitializeVmMemory(ctx context.Context, record *cvm_ram.Record) error { + return dp.cvmRam.InitializeMemory(ctx, record) } func (dp *DatabaseProvider) FreeVmMemoryByIndex(ctx context.Context, memoryAccount string, index uint16) error { - return dp.vmRam.FreeMemoryByIndex(ctx, memoryAccount, index) + return dp.cvmRam.FreeMemoryByIndex(ctx, memoryAccount, index) } func (dp *DatabaseProvider) FreeVmMemoryByAddress(ctx context.Context, address string) error { - return dp.vmRam.FreeMemoryByAddress(ctx, address) + return dp.cvmRam.FreeMemoryByAddress(ctx, address) } func (dp *DatabaseProvider) ReserveVmMemory(ctx context.Context, vm string, accountType cvm.VirtualAccountType, address string) (string, uint16, error) { - return dp.vmRam.ReserveMemory(ctx, vm, accountType, address) + return dp.cvmRam.ReserveMemory(ctx, vm, accountType, address) +} + +// VM Storage +// -------------------------------------------------------------------------------- +func (dp *DatabaseProvider) InitializeVmStorage(ctx context.Context, record *cvm_storage.Record) error { + return dp.cvmStorage.InitializeStorage(ctx, record) +} +func (dp *DatabaseProvider) FindAnyVmStorageWithAvailableCapacity(ctx context.Context, vm string, purpose cvm_storage.Purpose, minCapacity uint64) (*cvm_storage.Record, error) { + return dp.cvmStorage.FindAnyWithAvailableCapacity(ctx, vm, purpose, minCapacity) +} +func (dp *DatabaseProvider) ReserveVmStorage(ctx context.Context, vm string, purpose cvm_storage.Purpose, address string) (string, error) { + return dp.cvmStorage.ReserveStorage(ctx, vm, purpose, address) } diff --git a/pkg/solana/cvm/instructions_vm_storage_init.go b/pkg/solana/cvm/instructions_vm_storage_init.go index d5447300..d592808b 100644 --- a/pkg/solana/cvm/instructions_vm_storage_init.go +++ b/pkg/solana/cvm/instructions_vm_storage_init.go @@ -27,7 +27,6 @@ type VmStorageInitInstructionArgs struct { type VmStorageInitInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey - VmMemory ed25519.PublicKey VmStorage ed25519.PublicKey } From 68b136a42c2c2d86fa8da3c656bb6b583a7cb3ed Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 15 Aug 2024 13:07:27 -0400 Subject: [PATCH 27/79] Pull official version of the VM Indexer Service --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index c08dee34..d5636c22 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/code-payments/code-server -go 1.21.6 +go 1.23.0 require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 github.com/code-payments/code-protobuf-api v1.16.6 - github.com/code-payments/code-vm-indexer v0.0.0-20240722205247-52cedd8b587d + github.com/code-payments/code-vm-indexer v0.1.0 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 694baadf..4f1b80b5 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-protobuf-api v1.16.6 h1:QCot0U+4Ar5SdSX4v955FORMsd3Qcf0ZgkoqlGJZzu0= github.com/code-payments/code-protobuf-api v1.16.6/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= -github.com/code-payments/code-vm-indexer v0.0.0-20240722205247-52cedd8b587d h1:itYJseNRSiz/wreM40eUpBDKTmgIsGffgw7+Ls8YKeA= -github.com/code-payments/code-vm-indexer v0.0.0-20240722205247-52cedd8b587d/go.mod h1:zkX5TSEOWYTcr5c1OhLHEVcYbW6UePp/szWVPBI0wPQ= +github.com/code-payments/code-vm-indexer v0.1.0 h1:XzBwFrZp1R+9POGF/zMy5o6/OCI2J+jGJ7qr4cL72rY= +github.com/code-payments/code-vm-indexer v0.1.0/go.mod h1:LtXqlb7ub0mPUNKlCPJbsEDQrkZvWTPSRM5hTdHcqpM= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= From 3ed5d9063646976691bbce5765392a7d0dfec144 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 15 Aug 2024 14:24:30 -0400 Subject: [PATCH 28/79] Setup common VM instance public key (#169) --- pkg/code/async/nonce/config.go | 5 ---- pkg/code/async/nonce/metrics.go | 7 ++--- pkg/code/async/nonce/service.go | 3 +- .../async/sequencer/fulfillment_handler.go | 30 +++++++------------ pkg/code/async/vm/storage.go | 6 ++-- pkg/code/common/vm.go | 8 +++++ 6 files changed, 26 insertions(+), 33 deletions(-) create mode 100644 pkg/code/common/vm.go diff --git a/pkg/code/async/nonce/config.go b/pkg/code/async/nonce/config.go index 03e243f5..a1552673 100644 --- a/pkg/code/async/nonce/config.go +++ b/pkg/code/async/nonce/config.go @@ -8,9 +8,6 @@ import ( const ( envConfigPrefix = "NONCE_SERVICE_" - cvmPublicKeyConfigEnvName = envConfigPrefix + "CVM_PUBLIC_KEY" - defaultCvmPublicKey = "invalid" // Ensure something valid is set - solanaMainnetNoncePubkeyPrefixConfigEnvName = envConfigPrefix + "SOLANA_MAINNET_NONCE_PUBKEY_PREFIX" defaultSolanaMainnetNoncePubkeyPrefix = "non" @@ -19,7 +16,6 @@ const ( ) type conf struct { - cvmPublicKey config.String solanaMainnetNoncePubkeyPrefix config.String solanaMainnetNoncePoolSize config.Uint64 } @@ -31,7 +27,6 @@ type ConfigProvider func() *conf func WithEnvConfigs() ConfigProvider { return func() *conf { return &conf{ - cvmPublicKey: env.NewStringConfig(cvmPublicKeyConfigEnvName, defaultCvmPublicKey), solanaMainnetNoncePubkeyPrefix: env.NewStringConfig(solanaMainnetNoncePubkeyPrefixConfigEnvName, defaultSolanaMainnetNoncePubkeyPrefix), solanaMainnetNoncePoolSize: env.NewUint64Config(solanaMainnetNoncePoolSizeConfigEnvName, defaultSolanaMainnetNoncePoolSize), } diff --git a/pkg/code/async/nonce/metrics.go b/pkg/code/async/nonce/metrics.go index 3ba4a33a..b1846da4 100644 --- a/pkg/code/async/nonce/metrics.go +++ b/pkg/code/async/nonce/metrics.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/metrics" ) @@ -14,8 +15,6 @@ const ( ) func (p *service) metricsGaugeWorker(ctx context.Context) error { - cvmPublicKey := p.conf.cvmPublicKey.Get(ctx) - delay := time.Second for { @@ -44,11 +43,11 @@ func (p *service) metricsGaugeWorker(ctx context.Context) error { } recordNonceCountEvent(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, useCase, count) - count, err = p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentCvm, cvmPublicKey, state, useCase) + count, err = p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), state, useCase) if err != nil { continue } - recordNonceCountEvent(ctx, nonce.EnvironmentCvm, cvmPublicKey, state, useCase, count) + recordNonceCountEvent(ctx, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), state, useCase, count) } } diff --git a/pkg/code/async/nonce/service.go b/pkg/code/async/nonce/service.go index 4d001c9d..7e1c582b 100644 --- a/pkg/code/async/nonce/service.go +++ b/pkg/code/async/nonce/service.go @@ -10,6 +10,7 @@ import ( indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" "github.com/code-payments/code-server/pkg/code/async" + "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/nonce" ) @@ -67,7 +68,7 @@ func (p *service) Start(ctx context.Context, interval time.Duration) error { } { go func(state nonce.State) { - err := p.worker(ctx, nonce.EnvironmentCvm, p.conf.cvmPublicKey.Get(ctx), state, interval) + err := p.worker(ctx, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), state, interval) if err != nil && err != context.Canceled { p.log.WithError(err).Warnf("nonce processing loop terminated unexpectedly for state %d", state) } diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 22a00e38..8a18af28 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -130,8 +130,6 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) SupportsOnDemandTran } func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - var vm *common.Account // todo: configure vm account - if fulfillmentRecord.FulfillmentType != fulfillment.InitializeLockedTimelockAccount { return nil, errors.New("invalid fulfillment type") } @@ -146,12 +144,12 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransact return nil, err } - timelockAccounts, err := authorityAccount.GetTimelockAccounts(vm, common.KinMintAccount) + timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } - memory, accountIndex, err := reserveVmMemory(ctx, h.data, vm, cvm.VirtualAccountTypeTimelock, timelockAccounts.Vault) + memory, accountIndex, err := reserveVmMemory(ctx, h.data, common.CodeVmAccount, cvm.VirtualAccountTypeTimelock, timelockAccounts.Vault) if err != nil { return nil, err } @@ -703,8 +701,6 @@ func (h *TransferWithCommitmentFulfillmentHandler) SupportsOnDemandTransactions( } func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - var vm *common.Account // todo: configure vm account - commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) if err != nil { return nil, err @@ -753,12 +749,12 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, err } - _, timelockAccountMemory, timelockAccountIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vm, destinationTimelockOwner) + _, timelockAccountMemory, timelockAccountIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationTimelockOwner) if err != nil { return nil, err } - relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, vm, cvm.VirtualAccountTypeRelay, commitment) + relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, common.CodeVmAccount, cvm.VirtualAccountTypeRelay, commitment) if err != nil { return nil, err } @@ -768,7 +764,7 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c selectedNonce.Account, selectedNonce.Blockhash, - vm, + common.CodeVmAccount, timelockAccountMemory, timelockAccountIndex, relayMemory, @@ -873,8 +869,6 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) SupportsOnDemandTransactio } func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - var vm *common.Account // todo: configure vm account - if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { return nil, errors.New("invalid fulfillment type") } @@ -892,7 +886,7 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ct return nil, err } - virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vm, timelockOwner) + virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, timelockOwner) if err != nil { return nil, err } @@ -901,7 +895,7 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ct return nil, errors.New("stale timelock account state") } - storage, err := reserveVmStorage(ctx, h.data, vm, storage.PurposeDeletion, timelockVault) + storage, err := reserveVmStorage(ctx, h.data, common.CodeVmAccount, storage.PurposeDeletion, timelockVault) if err != nil { return nil, err } @@ -910,7 +904,7 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ct selectedNonce.Account, selectedNonce.Blockhash, - vm, + common.CodeVmAccount, memory, index, storage, @@ -1055,8 +1049,6 @@ func (h *CloseCommitmentFulfillmentHandler) SupportsOnDemandTransactions() bool } func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - var vm *common.Account // todo: configure vm account - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { return nil, errors.New("invalid fulfillment type") } @@ -1066,12 +1058,12 @@ func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context. return nil, err } - virtualAccountState, memory, index, err := getVirtualRelayAccountStateInMemory(ctx, h.vmIndexerClient, vm, relay) + virtualAccountState, memory, index, err := getVirtualRelayAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, relay) if err != nil { return nil, err } - storage, err := reserveVmStorage(ctx, h.data, vm, storage.PurposeDeletion, relay) + storage, err := reserveVmStorage(ctx, h.data, common.CodeVmAccount, storage.PurposeDeletion, relay) if err != nil { return nil, err } @@ -1080,7 +1072,7 @@ func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context. selectedNonce.Account, selectedNonce.Blockhash, - vm, + common.CodeVmAccount, memory, index, storage, diff --git a/pkg/code/async/vm/storage.go b/pkg/code/async/vm/storage.go index 08cedd8d..c77aed0d 100644 --- a/pkg/code/async/vm/storage.go +++ b/pkg/code/async/vm/storage.go @@ -54,12 +54,10 @@ func (p *service) storageInitWorker(serviceCtx context.Context, interval time.Du } func (p *service) maybeInitStorageAccount(ctx context.Context) error { - var vm *common.Account // todo: configure vm account - // todo: iterate over purposes when we have more than one purpose := storage.PurposeDeletion - _, err := p.data.FindAnyVmStorageWithAvailableCapacity(ctx, vm.PublicKey().ToBase58(), purpose, minStorageAccountCapacity) + _, err := p.data.FindAnyVmStorageWithAvailableCapacity(ctx, common.CodeVmAccount.PublicKey().ToBase58(), purpose, minStorageAccountCapacity) switch err { case storage.ErrNotFound: case nil: @@ -68,7 +66,7 @@ func (p *service) maybeInitStorageAccount(ctx context.Context) error { return err } - record, err := p.initStorageAccountOnBlockchain(ctx, vm, purpose) + record, err := p.initStorageAccountOnBlockchain(ctx, common.CodeVmAccount, purpose) if err != nil { return err } diff --git a/pkg/code/common/vm.go b/pkg/code/common/vm.go new file mode 100644 index 00000000..5c2cdc5b --- /dev/null +++ b/pkg/code/common/vm.go @@ -0,0 +1,8 @@ +package common + +var ( + // The well-known Code VM instance used by the Code app + // + // todo: real public key once program is deployed and VM instance is initialized + CodeVmAccount, _ = NewAccountFromPublicKeyString("BkwoMG33cgSDrc3fEjfhZufqzYC3icXTTMajuueXyYGG") +) From 28d7a26d6cf3b48c2dd5edb7a2029e6318b7ca0a Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 21 Aug 2024 14:01:58 -0400 Subject: [PATCH 29/79] Update SubmitIntent for VM intent creations (#171) * Pull in protobuf APIs from vm branch * String out unnecessary stuff in transaction service not required for cash * Update OpenAccounts, SendPrivatePayment and ReceivePaymentsPrivately intent validation * Remove now unused features in SubmitIntent * Temporarily remove more privacy upgrade stuff * External transfers are not supported yet * Update SubmitIntent action signature flow * Add todo note on nonce selection for upgrade actions * Replace legacy splitter_token package for deriving commitment address * Fix private transfers to use the commitment vault versus commitment state address * Bring back other intent types * Disable remote send and tipping since there are currently unknown strategies * Update vault_address field in postgres commitment store to previous value --- go.mod | 2 +- go.sum | 10 + .../commitment/temporary_privacy_test.go | 2 +- pkg/code/async/commitment/testutil.go | 5 +- pkg/code/async/commitment/transaction.go | 2 +- .../async/sequencer/fulfillment_handler.go | 9 +- pkg/code/data/commitment/commitment.go | 11 +- pkg/code/data/commitment/memory/store.go | 21 + pkg/code/data/commitment/postgres/model.go | 45 +- pkg/code/data/commitment/postgres/store.go | 10 + .../data/commitment/postgres/store_test.go | 2 + pkg/code/data/commitment/store.go | 3 + pkg/code/data/commitment/tests/tests.go | 27 +- pkg/code/data/internal.go | 4 + .../grpc/transaction/v2/action_handler.go | 382 ++++---------- .../server/grpc/transaction/v2/airdrop.go | 231 +-------- .../grpc/transaction/v2/airdrop_test.go | 6 +- pkg/code/server/grpc/transaction/v2/errors.go | 29 +- .../server/grpc/transaction/v2/history.go | 289 ----------- .../grpc/transaction/v2/history_test.go | 424 ---------------- pkg/code/server/grpc/transaction/v2/intent.go | 241 +++------ .../grpc/transaction/v2/intent_handler.go | 469 ++---------------- .../server/grpc/transaction/v2/intent_test.go | 2 + .../server/grpc/transaction/v2/limits_test.go | 2 + .../grpc/transaction/v2/local_simulation.go | 33 +- .../transaction/v2/local_simulation_test.go | 2 + .../server/grpc/transaction/v2/onramp_test.go | 2 + pkg/code/server/grpc/transaction/v2/proof.go | 19 +- pkg/code/server/grpc/transaction/v2/swap.go | 8 +- .../server/grpc/transaction/v2/testutil.go | 2 + pkg/code/transaction/virtual_instruction.go | 94 ++++ 31 files changed, 488 insertions(+), 1900 deletions(-) delete mode 100644 pkg/code/server/grpc/transaction/v2/history.go delete mode 100644 pkg/code/server/grpc/transaction/v2/history_test.go create mode 100644 pkg/code/transaction/virtual_instruction.go diff --git a/go.mod b/go.mod index d5636c22..b471c6a8 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.6 + github.com/code-payments/code-protobuf-api v1.18.1-0.20240820154350-ad076084d5a4 github.com/code-payments/code-vm-indexer v0.1.0 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 diff --git a/go.sum b/go.sum index 4f1b80b5..033ec059 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,16 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-protobuf-api v1.16.6 h1:QCot0U+4Ar5SdSX4v955FORMsd3Qcf0ZgkoqlGJZzu0= github.com/code-payments/code-protobuf-api v1.16.6/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240816181522-d676c469560e h1:I7bUSQWqDeJ1pjJo58MFFiBraocNXpxzKYT/ZQRAYK8= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240816181522-d676c469560e/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240820134923-fef9aa9ba43c h1:OT6lfsDtao85Yxosdkm5QpPMdwWp4uln63450iEvPn0= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240820134923-fef9aa9ba43c/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240820143954-42db9b3554ca h1:xRsh9a/PZyouBpo1IBwdrVnCZZRmvlyDQlS1OJKAr/w= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240820143954-42db9b3554ca/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240820144322-178f09e6cb66 h1:9FXVlKe7qYXv2s3YU3z+ptxbVFiPtIwPRehIlTTntPM= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240820144322-178f09e6cb66/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240820154350-ad076084d5a4 h1:f/dui6iRfeYm/59hsh3gC9UU016po77bp1TdLnBuzG8= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240820154350-ad076084d5a4/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/code-payments/code-vm-indexer v0.1.0 h1:XzBwFrZp1R+9POGF/zMy5o6/OCI2J+jGJ7qr4cL72rY= github.com/code-payments/code-vm-indexer v0.1.0/go.mod h1:LtXqlb7ub0mPUNKlCPJbsEDQrkZvWTPSRM5hTdHcqpM= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= diff --git a/pkg/code/async/commitment/temporary_privacy_test.go b/pkg/code/async/commitment/temporary_privacy_test.go index d2e5efc6..23ee31c1 100644 --- a/pkg/code/async/commitment/temporary_privacy_test.go +++ b/pkg/code/async/commitment/temporary_privacy_test.go @@ -37,7 +37,7 @@ func TestGetDeadlineToUpgradePrivacy_HappyPath(t *testing.T) { commitmentRecord.TreasuryRepaid = false // Privacy is already upgraded - commitmentRecord.RepaymentDivertedTo = &commitmentRecords[1].Address + commitmentRecord.RepaymentDivertedTo = &commitmentRecords[1].VaultAddress _, err = GetDeadlineToUpgradePrivacy(env.ctx, env.data, commitmentRecord) assert.Equal(t, ErrNoPrivacyUpgradeDeadline, err) commitmentRecord.RepaymentDivertedTo = nil diff --git a/pkg/code/async/commitment/testutil.go b/pkg/code/async/commitment/testutil.go index 5a039124..4e143d0b 100644 --- a/pkg/code/async/commitment/testutil.go +++ b/pkg/code/async/commitment/testutil.go @@ -106,7 +106,8 @@ func (e testEnv) simulateCommitment(t *testing.T, recentRoot string, state commi require.NoError(t, err) commitmentRecord := &commitment.Record{ - Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + VaultAddress: testutil.NewRandomAccount(t).PublicKey().ToBase58(), Pool: e.treasuryPool.Address, RecentRoot: recentRoot, @@ -259,7 +260,7 @@ func (e testEnv) simulatePermanentPrivacyChequeCashed(t *testing.T, commitmentRe func (e testEnv) simulateCommitmentBeingUpgraded(t *testing.T, upgradeFrom, upgradeTo *commitment.Record) { require.Nil(t, upgradeFrom.RepaymentDivertedTo) - upgradeFrom.RepaymentDivertedTo = &upgradeTo.Address + upgradeFrom.RepaymentDivertedTo = &upgradeTo.VaultAddress require.NoError(t, e.data.SaveCommitment(e.ctx, upgradeFrom)) fulfillmentRecords, err := e.data.GetAllFulfillmentsByTypeAndAction(e.ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, upgradeFrom.Intent, upgradeFrom.ActionId) diff --git a/pkg/code/async/commitment/transaction.go b/pkg/code/async/commitment/transaction.go index 0031e1bd..0d261e8e 100644 --- a/pkg/code/async/commitment/transaction.go +++ b/pkg/code/async/commitment/transaction.go @@ -33,7 +33,7 @@ func (p *service) injectCloseCommitmentFulfillment(ctx context.Context, commitme FulfillmentType: fulfillment.CloseCommitment, - Source: commitmentRecord.Address, + Source: commitmentRecord.VaultAddress, IntentOrderingIndex: uint64(math.MaxInt64), ActionOrderingIndex: 0, diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 8a18af28..7184ef8e 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -541,7 +541,7 @@ func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) CanSubmitToBlo return false, nil } - newCommitmentRecord, err := h.data.GetCommitmentByAddress(ctx, *oldCommitmentRecord.RepaymentDivertedTo) + newCommitmentRecord, err := h.data.GetCommitmentByVault(ctx, *oldCommitmentRecord.RepaymentDivertedTo) if err != nil { return false, err } @@ -739,6 +739,11 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, err } + commitmentVault, err := common.NewAccountFromPublicKeyString(commitmentRecord.VaultAddress) + if err != nil { + return nil, err + } + transcript, err := hex.DecodeString(commitmentRecord.Transcript) if err != nil { return nil, err @@ -754,7 +759,7 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, err } - relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, common.CodeVmAccount, cvm.VirtualAccountTypeRelay, commitment) + relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, common.CodeVmAccount, cvm.VirtualAccountTypeRelay, commitmentVault) if err != nil { return nil, err } diff --git a/pkg/code/data/commitment/commitment.go b/pkg/code/data/commitment/commitment.go index 0d1fe459..bf2ded5b 100644 --- a/pkg/code/data/commitment/commitment.go +++ b/pkg/code/data/commitment/commitment.go @@ -27,7 +27,8 @@ const ( type Record struct { Id uint64 - Address string + Address string + VaultAddress string Pool string RecentRoot string @@ -58,6 +59,10 @@ func (r *Record) Validate() error { return errors.New("address is required") } + if len(r.VaultAddress) == 0 { + return errors.New("vault address is required") + } + if len(r.Pool) == 0 { return errors.New("pool is required") } @@ -93,7 +98,8 @@ func (r *Record) Clone() *Record { return &Record{ Id: r.Id, - Address: r.Address, + Address: r.Address, + VaultAddress: r.VaultAddress, Pool: r.Pool, RecentRoot: r.RecentRoot, @@ -120,6 +126,7 @@ func (r *Record) CopyTo(dst *Record) { dst.Id = r.Id dst.Address = r.Address + dst.VaultAddress = r.VaultAddress dst.Pool = r.Pool dst.RecentRoot = r.RecentRoot diff --git a/pkg/code/data/commitment/memory/store.go b/pkg/code/data/commitment/memory/store.go index 26bb6f7a..8261036d 100644 --- a/pkg/code/data/commitment/memory/store.go +++ b/pkg/code/data/commitment/memory/store.go @@ -84,6 +84,18 @@ func (s *store) GetByAddress(_ context.Context, address string) (*commitment.Rec return nil, commitment.ErrCommitmentNotFound } +// GetByVault implements commitment.Store.GetByVault +func (s *store) GetByVault(_ context.Context, address string) (*commitment.Record, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if item := s.findByVault(address); item != nil { + return item.Clone(), nil + } + + return nil, commitment.ErrCommitmentNotFound +} + // GetByAction implements commitment.Store.GetByAction func (s *store) GetByAction(_ context.Context, intentId string, actionId uint32) (*commitment.Record, error) { s.mu.Lock() @@ -201,6 +213,15 @@ func (s *store) findByAddress(address string) *commitment.Record { return nil } +func (s *store) findByVault(address string) *commitment.Record { + for _, item := range s.records { + if item.VaultAddress == address { + return item + } + } + return nil +} + func (s *store) findByAction(intentId string, actionId uint32) *commitment.Record { for _, item := range s.records { if item.Intent == intentId && item.ActionId == actionId { diff --git a/pkg/code/data/commitment/postgres/model.go b/pkg/code/data/commitment/postgres/model.go index b188fd43..295e0aa2 100644 --- a/pkg/code/data/commitment/postgres/model.go +++ b/pkg/code/data/commitment/postgres/model.go @@ -20,7 +20,8 @@ const ( type model struct { Id sql.NullInt64 `db:"id"` - Address string `db:"address"` + Address string `db:"address"` + VaultAddress string `db:"vault"` Pool string `db:"pool"` RecentRoot string `db:"recent_root"` @@ -48,7 +49,8 @@ func toModel(obj *commitment.Record) (*model, error) { } return &model{ - Address: obj.Address, + Address: obj.Address, + VaultAddress: obj.VaultAddress, Pool: obj.Pool, RecentRoot: obj.RecentRoot, @@ -78,7 +80,8 @@ func fromModel(obj *model) *commitment.Record { return &commitment.Record{ Id: uint64(obj.Id.Int64), - Address: obj.Address, + Address: obj.Address, + VaultAddress: obj.VaultAddress, Pool: obj.Pool, RecentRoot: obj.RecentRoot, @@ -105,7 +108,7 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { divertedToCondition := tableName + ".repayment_diverted_to IS NULL" if m.RepaymentDivertedTo.Valid { - divertedToCondition = "(" + tableName + ".repayment_diverted_to IS NULL OR " + tableName + ".repayment_diverted_to = $11)" + divertedToCondition = "(" + tableName + ".repayment_diverted_to IS NULL OR " + tableName + ".repayment_diverted_to = $12)" } treasuryRepaidCondition := tableName + ".treasury_repaid IS FALSE" @@ -118,16 +121,16 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { // Luckily, all updateable state-like fields should progress forward in a // predictable manner, making conditions easy to reason about. query := `INSERT INTO ` + tableName + ` - (address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + (address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (address) DO UPDATE - SET treasury_repaid = $10 OR ` + tableName + `.treasury_repaid , repayment_diverted_to = COALESCE($11, ` + tableName + `.repayment_diverted_to), state = GREATEST($12, ` + tableName + `.state) - WHERE ` + tableName + `.address = $1 AND ` + treasuryRepaidCondition + ` AND ` + divertedToCondition + ` AND ` + tableName + `.state <= $12 + SET treasury_repaid = $11 OR ` + tableName + `.treasury_repaid , repayment_diverted_to = COALESCE($12, ` + tableName + `.repayment_diverted_to), state = GREATEST($13, ` + tableName + `.state) + WHERE ` + tableName + `.address = $1 AND ` + treasuryRepaidCondition + ` AND ` + divertedToCondition + ` AND ` + tableName + `.state <= $13 RETURNING - id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at` + id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at` if m.CreatedAt.IsZero() { m.CreatedAt = time.Now() @@ -137,6 +140,7 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { ctx, query, m.Address, + m.VaultAddress, m.Pool, m.RecentRoot, m.Transcript, @@ -158,7 +162,7 @@ func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*model, error) { res := &model{} - query := `SELECT id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at FROM ` + tableName + ` WHERE address = $1 LIMIT 1` @@ -170,10 +174,25 @@ func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*model, e return res, nil } +func dbGetByVault(ctx context.Context, db *sqlx.DB, address string) (*model, error) { + res := &model{} + + query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + FROM ` + tableName + ` + WHERE vault = $1 + LIMIT 1` + + err := db.GetContext(ctx, res, query, address) + if err != nil { + return nil, pgutil.CheckNoRows(err, commitment.ErrCommitmentNotFound) + } + return res, nil +} + func dbGetByAction(ctx context.Context, db *sqlx.DB, intentId string, actionId uint32) (*model, error) { res := &model{} - query := `SELECT id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at FROM ` + tableName + ` WHERE intent = $1 AND action_id = $2 LIMIT 1` @@ -188,7 +207,7 @@ func dbGetByAction(ctx context.Context, db *sqlx.DB, intentId string, actionId u func dbGetAllByState(ctx context.Context, db *sqlx.DB, state commitment.State, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*model, error) { res := []*model{} - query := `SELECT id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at FROM ` + tableName + ` WHERE (state = $1) ` @@ -210,7 +229,7 @@ func dbGetAllByState(ctx context.Context, db *sqlx.DB, state commitment.State, c func dbGetUpgradeableByOwner(ctx context.Context, db *sqlx.DB, owner string, limit uint64) ([]*model, error) { res := []*model{} - query := `SELECT id, address, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at + query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at FROM ` + tableName + ` WHERE owner = $1 AND state > $3 AND state < $4 AND repayment_diverted_to IS NULL LIMIT $2 diff --git a/pkg/code/data/commitment/postgres/store.go b/pkg/code/data/commitment/postgres/store.go index eabff4bc..1f9f17a4 100644 --- a/pkg/code/data/commitment/postgres/store.go +++ b/pkg/code/data/commitment/postgres/store.go @@ -48,6 +48,16 @@ func (s *store) GetByAddress(ctx context.Context, address string) (*commitment.R return fromModel(model), nil } +// GetByVault implements commitment.Store.GetByVault +func (s *store) GetByVault(ctx context.Context, address string) (*commitment.Record, error) { + model, err := dbGetByVault(ctx, s.db, address) + if err != nil { + return nil, err + } + + return fromModel(model), nil +} + // GetByAction implements commitment.Store.GetByAction func (s *store) GetByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) { model, err := dbGetByAction(ctx, s.db, intentId, actionId) diff --git a/pkg/code/data/commitment/postgres/store_test.go b/pkg/code/data/commitment/postgres/store_test.go index cc3c8598..9f8fb196 100644 --- a/pkg/code/data/commitment/postgres/store_test.go +++ b/pkg/code/data/commitment/postgres/store_test.go @@ -23,6 +23,7 @@ const ( id SERIAL NOT NULL PRIMARY KEY, address TEXT NOT NULL, + vault TEXT NOT NULL, pool TEXT NOT NULL, recent_root TEXT NOT NULL, @@ -44,6 +45,7 @@ const ( created_at TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT codewallet__core_commitment__uniq__address UNIQUE (address), + CONSTRAINT codewallet__core_commitment__uniq__vault_address UNIQUE (vault_address), CONSTRAINT codewallet__core_commitment__uniq__transcript UNIQUE (transcript), CONSTRAINT codewallet__core_commitment__uniq__intent__and__action_id UNIQUE (intent, action_id) ); diff --git a/pkg/code/data/commitment/store.go b/pkg/code/data/commitment/store.go index 647080d1..96cc1ea8 100644 --- a/pkg/code/data/commitment/store.go +++ b/pkg/code/data/commitment/store.go @@ -19,6 +19,9 @@ type Store interface { // GetByAddress gets a commitment account's state by its address GetByAddress(ctx context.Context, address string) (*Record, error) + // GetByVault gets a commitment account's state by its vault address + GetByVault(ctx context.Context, address string) (*Record, error) + // GetByAction gets a commitment account's state by the action it's involved in GetByAction(ctx context.Context, intentId string, actionId uint32) (*Record, error) diff --git a/pkg/code/data/commitment/tests/tests.go b/pkg/code/data/commitment/tests/tests.go index 6d2d2e24..47060b4d 100644 --- a/pkg/code/data/commitment/tests/tests.go +++ b/pkg/code/data/commitment/tests/tests.go @@ -33,7 +33,8 @@ func testRoundTrip(t *testing.T, s commitment.Store) { ctx := context.Background() expected := &commitment.Record{ - Address: "address", + Address: "address", + VaultAddress: "vault", Pool: "pool", RecentRoot: "root", @@ -55,6 +56,9 @@ func testRoundTrip(t *testing.T, s commitment.Store) { _, err := s.GetByAddress(ctx, expected.Address) assert.Equal(t, commitment.ErrCommitmentNotFound, err) + _, err = s.GetByVault(ctx, expected.VaultAddress) + assert.Equal(t, commitment.ErrCommitmentNotFound, err) + _, err = s.GetByAction(ctx, expected.Intent, expected.ActionId) assert.Equal(t, commitment.ErrCommitmentNotFound, err) @@ -81,6 +85,10 @@ func testRoundTrip(t *testing.T, s commitment.Store) { require.NoError(t, err) assertEquivalentRecords(t, expected, actual) + actual, err = s.GetByVault(ctx, expected.VaultAddress) + require.NoError(t, err) + assertEquivalentRecords(t, expected, actual) + actual, err = s.GetByAction(ctx, expected.Intent, expected.ActionId) require.NoError(t, err) assertEquivalentRecords(t, expected, actual) @@ -94,7 +102,8 @@ func testUpdateConstraints(t *testing.T, s commitment.Store) { otherCommitmentCommitment1 := "other-commitment-1" otherCommitmentCommitment2 := "other-commitment-2" expected := &commitment.Record{ - Address: "address", + Address: "address", + VaultAddress: "vault", Pool: "pool", RecentRoot: "root", @@ -150,11 +159,11 @@ func testGetAllByState(t *testing.T, s commitment.Store) { assert.Equal(t, commitment.ErrCommitmentNotFound, err) expected := []*commitment.Record{ - {Address: "commitment1", Pool: "pool", RecentRoot: "root", Transcript: "transcript1", Intent: "intent", ActionId: 0, Owner: "owner1", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {Address: "commitment2", Pool: "pool", RecentRoot: "root", Transcript: "transcript2", Intent: "intent", ActionId: 1, Owner: "owner2", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {Address: "commitment3", Pool: "pool", RecentRoot: "root", Transcript: "transcript3", Intent: "intent", ActionId: 2, Owner: "owner3", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {Address: "commitment4", Pool: "pool", RecentRoot: "root", Transcript: "transcript4", Intent: "intent", ActionId: 3, Owner: "owner4", Destination: "destination", Amount: 123, State: commitment.StateClosed}, - {Address: "commitment5", Pool: "pool", RecentRoot: "root", Transcript: "transcript5", Intent: "intent", ActionId: 4, Owner: "owner5", Destination: "destination", Amount: 123, State: commitment.StateClosed}, + {Address: "commitment1", VaultAddress: "vault1", Pool: "pool", RecentRoot: "root", Transcript: "transcript1", Intent: "intent", ActionId: 0, Owner: "owner1", Destination: "destination", Amount: 123, State: commitment.StateOpen}, + {Address: "commitment2", VaultAddress: "vault2", Pool: "pool", RecentRoot: "root", Transcript: "transcript2", Intent: "intent", ActionId: 1, Owner: "owner2", Destination: "destination", Amount: 123, State: commitment.StateOpen}, + {Address: "commitment3", VaultAddress: "vault3", Pool: "pool", RecentRoot: "root", Transcript: "transcript3", Intent: "intent", ActionId: 2, Owner: "owner3", Destination: "destination", Amount: 123, State: commitment.StateOpen}, + {Address: "commitment4", VaultAddress: "vault4", Pool: "pool", RecentRoot: "root", Transcript: "transcript4", Intent: "intent", ActionId: 3, Owner: "owner4", Destination: "destination", Amount: 123, State: commitment.StateClosed}, + {Address: "commitment5", VaultAddress: "vault5", Pool: "pool", RecentRoot: "root", Transcript: "transcript5", Intent: "intent", ActionId: 4, Owner: "owner5", Destination: "destination", Amount: 123, State: commitment.StateClosed}, } for _, record := range expected { require.NoError(t, s.Save(ctx, record)) @@ -249,6 +258,7 @@ func testGetUpgradeableByOwner(t *testing.T, s commitment.Store) { record.Pool = "pool" record.RecentRoot = "root" record.Address = fmt.Sprintf("address%d", i) + record.VaultAddress = fmt.Sprintf("vault%d", i) record.RecentRoot = fmt.Sprintf("root%d", i) record.Transcript = fmt.Sprintf("transcript%d", i) record.Destination = fmt.Sprintf("destination%d", i) @@ -306,6 +316,7 @@ func testGetTreasuryPoolDeficit(t *testing.T, s commitment.Store) { // Populate data irrelevant to test record.Address = fmt.Sprintf("address%d", i) + record.VaultAddress = fmt.Sprintf("vault%d", i) record.RecentRoot = fmt.Sprintf("root%d", i) record.Transcript = fmt.Sprintf("transcript%d", i) record.Destination = fmt.Sprintf("destination%d", i) @@ -367,6 +378,7 @@ func testCounts(t *testing.T, s commitment.Store) { record.Pool = "pool" record.Amount = 1 record.Address = fmt.Sprintf("address%d", i) + record.VaultAddress = fmt.Sprintf("vault%d", i) record.Transcript = fmt.Sprintf("transcript%d", i) record.Destination = fmt.Sprintf("destination%d", i) record.Intent = fmt.Sprintf("intent%d", i) @@ -405,6 +417,7 @@ func testCounts(t *testing.T, s commitment.Store) { func assertEquivalentRecords(t *testing.T, obj1, obj2 *commitment.Record) { assert.Equal(t, obj1.Address, obj2.Address) + assert.Equal(t, obj1.VaultAddress, obj2.VaultAddress) assert.Equal(t, obj1.Pool, obj2.Pool) assert.Equal(t, obj1.RecentRoot, obj2.RecentRoot) assert.Equal(t, obj1.Transcript, obj2.Transcript) diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index c49794f3..616bb662 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -325,6 +325,7 @@ type DatabaseData interface { // -------------------------------------------------------------------------------- SaveCommitment(ctx context.Context, record *commitment.Record) error GetCommitmentByAddress(ctx context.Context, address string) (*commitment.Record, error) + GetCommitmentByVault(ctx context.Context, vault string) (*commitment.Record, error) GetCommitmentByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) GetAllCommitmentsByState(ctx context.Context, state commitment.State, opts ...query.Option) ([]*commitment.Record, error) GetUpgradeableCommitmentsByOwner(ctx context.Context, owner string, limit uint64) ([]*commitment.Record, error) @@ -1286,6 +1287,9 @@ func (dp *DatabaseProvider) SaveCommitment(ctx context.Context, record *commitme func (dp *DatabaseProvider) GetCommitmentByAddress(ctx context.Context, address string) (*commitment.Record, error) { return dp.commitment.GetByAddress(ctx, address) } +func (dp *DatabaseProvider) GetCommitmentByVault(ctx context.Context, vault string) (*commitment.Record, error) { + return dp.commitment.GetByVault(ctx, vault) +} func (dp *DatabaseProvider) GetCommitmentByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) { return dp.commitment.GetByAction(ctx, intentId, actionId) } diff --git a/pkg/code/server/grpc/transaction/v2/action_handler.go b/pkg/code/server/grpc/transaction/v2/action_handler.go index 34a69625..09908d8e 100644 --- a/pkg/code/server/grpc/transaction/v2/action_handler.go +++ b/pkg/code/server/grpc/transaction/v2/action_handler.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "errors" "fmt" - "math" "time" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" @@ -23,28 +22,27 @@ import ( "github.com/code-payments/code-server/pkg/code/data/timelock" transaction_util "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/solana" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" + "github.com/code-payments/code-server/pkg/solana/cvm" ) -// todo: a better name for this lol? -type makeSolanaTransactionResult struct { - isCreatedOnDemand bool - txn *solana.Transaction // Can be null if the transaction is on-demand created at scheduling time +type newFulfillmentMetadata struct { + // Signature metadata + + requiresClientSignature bool + expectedSigner *common.Account // Must be null if the requiresClientSignature is false + virtualIxnHash *cvm.Hash // Must be null if the requiresClientSignature is false // Additional metadata to add to the action and fulfillment record, which relates - // specifically to the transaction that was created. + // specifically to the transaction or virtual instruction within the context of + // the action. fulfillmentType fulfillment.Type source *common.Account destination *common.Account - intentOrderingIndexOverride *uint64 - actionOrderingIndexOverride *uint32 - fulfillmentOrderingIndex uint32 - - disableActiveScheduling bool + fulfillmentOrderingIndex uint32 + disableActiveScheduling bool } // BaseActionHandler is a base interface for operation-specific action handlers @@ -67,28 +65,24 @@ type BaseActionHandler interface { type CreateActionHandler interface { BaseActionHandler - // TransactionCount returns the total number of transactions that will be created - // for the action. - TransactionCount() int + // FulfillmentCount returns the total number of fulfillments that + // will be created for the action. + FulfillmentCount() int // PopulateMetadata populates action metadata into the provided record PopulateMetadata(actionRecord *action.Record) error // RequiresNonce determines whether a nonce should be acquired for the - // transaction being created. This should only be false in cases where - // client signatures are not required and transaction construction can - // be deferred to scheduling time. The nonce and bh parameters of - // MakeNewSolanaTransaction will be null. - RequiresNonce(transactionIndex int) bool - - // MakeNewSolanaTransaction makes a new Solana transaction. Implementations - // can choose to defer creation until scheduling time. This may be done in - // cases where the client signature is not required. - MakeNewSolanaTransaction( + // fulfillment being created. This should be true whenever a virtual + // instruction needs to be signed by the client. + RequiresNonce(fulfillmentIndex int) bool + + // GetFulfillmentMetadata gets metadata for the fulfillment being created + GetFulfillmentMetadata( index int, nonce *common.Account, bh solana.Blockhash, - ) (*makeSolanaTransactionResult, error) + ) (*newFulfillmentMetadata, error) } // UpgradeActionHandler is an interface for upgrading existing actions. It's @@ -100,11 +94,11 @@ type UpgradeActionHandler interface { // upgraded. GetFulfillmentBeingUpgraded() *fulfillment.Record - // MakeUpgradedSolanaTransaction makes an upgraded Solana transaction - MakeUpgradedSolanaTransaction( + // GetFulfillmentMetadata gets upgraded fulfillment metadata + GetFulfillmentMetadata( nonce *common.Account, bh solana.Blockhash, - ) (*makeSolanaTransactionResult, error) + ) (*newFulfillmentMetadata, error) } type OpenAccountActionHandler struct { @@ -128,7 +122,7 @@ func NewOpenAccountActionHandler(data code_data.Provider, protoAction *transacti return nil, err } - timelockAccounts, err := authority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } @@ -165,7 +159,7 @@ func NewOpenAccountActionHandler(data code_data.Provider, protoAction *transacti }, nil } -func (h *OpenAccountActionHandler) TransactionCount() int { +func (h *OpenAccountActionHandler) FulfillmentCount() int { return 1 } @@ -189,16 +183,17 @@ func (h *OpenAccountActionHandler) RequiresNonce(index int) bool { return false } -func (h *OpenAccountActionHandler) MakeNewSolanaTransaction( +func (h *OpenAccountActionHandler) GetFulfillmentMetadata( index int, nonce *common.Account, bh solana.Blockhash, -) (*makeSolanaTransactionResult, error) { +) (*newFulfillmentMetadata, error) { switch index { case 0: - return &makeSolanaTransactionResult{ - isCreatedOnDemand: true, - txn: nil, + return &newFulfillmentMetadata{ + requiresClientSignature: false, + expectedSigner: nil, + virtualIxnHash: nil, fulfillmentType: fulfillment.InitializeLockedTimelockAccount, source: h.timelockAccounts.Vault, @@ -207,7 +202,7 @@ func (h *OpenAccountActionHandler) MakeNewSolanaTransaction( disableActiveScheduling: h.accountType != commonpb.AccountType_PRIMARY, // Non-primary accounts are created on demand after first usage }, nil default: - return nil, errors.New("invalid transaction index") + return nil, errors.New("invalid virtual ixn index") } } @@ -220,203 +215,6 @@ func (h *OpenAccountActionHandler) OnSaveToDB(ctx context.Context) error { return h.data.CreateAccountInfo(ctx, h.unsavedAccountInfoRecord) } -type CloseEmptyAccountActionHandler struct { - timelockAccounts *common.TimelockAccounts - intentType intent.Type -} - -func NewCloseEmptyAccountActionHandler(intentType intent.Type, protoAction *transactionpb.CloseEmptyAccountAction) (CreateActionHandler, error) { - authority, err := common.NewAccountFromProto(protoAction.Authority) - if err != nil { - return nil, err - } - - dataVersion := timelock_token_v1.DataVersion1 - if intentType == intent.MigrateToPrivacy2022 { - dataVersion = timelock_token_v1.DataVersionLegacy - } - timelockAccounts, err := authority.GetTimelockAccounts(dataVersion, common.KinMintAccount) - if err != nil { - return nil, err - } - - return &CloseEmptyAccountActionHandler{ - timelockAccounts: timelockAccounts, - intentType: intentType, - }, nil -} - -func (h *CloseEmptyAccountActionHandler) TransactionCount() int { - return 1 -} - -func (h *CloseEmptyAccountActionHandler) PopulateMetadata(actionRecord *action.Record) error { - actionRecord.Source = h.timelockAccounts.Vault.PublicKey().ToBase58() - - actionRecord.State = action.StatePending - - return nil -} - -func (h *CloseEmptyAccountActionHandler) GetServerParameter() *transactionpb.ServerParameter { - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_CloseEmptyAccount{ - CloseEmptyAccount: &transactionpb.CloseEmptyAccountServerParameter{}, - }, - } -} - -func (h *CloseEmptyAccountActionHandler) RequiresNonce(index int) bool { - return true -} - -func (h *CloseEmptyAccountActionHandler) MakeNewSolanaTransaction( - index int, - nonce *common.Account, - bh solana.Blockhash, -) (*makeSolanaTransactionResult, error) { - switch index { - case 0: - txn, err := transaction_util.MakeCloseEmptyAccountTransaction( - nonce, - bh, - h.timelockAccounts, - ) - if err != nil { - return nil, err - } - - return &makeSolanaTransactionResult{ - txn: &txn, - - fulfillmentType: fulfillment.CloseEmptyTimelockAccount, - source: h.timelockAccounts.Vault, - destination: nil, - fulfillmentOrderingIndex: 0, - disableActiveScheduling: h.intentType == intent.ReceivePaymentsPrivately, - }, nil - default: - return nil, errors.New("invalid transaction index") - } -} - -func (h *CloseEmptyAccountActionHandler) OnSaveToDB(ctx context.Context) error { - return nil -} - -type CloseDormantAccountActionHandler struct { - accountType commonpb.AccountType - source *common.TimelockAccounts - destination *common.Account -} - -func NewCloseDormantAccountActionHandler(protoAction *transactionpb.CloseDormantAccountAction) (CreateActionHandler, error) { - sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) - if err != nil { - return nil, err - } - - source, err := sourceAuthority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) - if err != nil { - return nil, err - } - - destination, err := common.NewAccountFromProto(protoAction.Destination) - if err != nil { - return nil, err - } - - return &CloseDormantAccountActionHandler{ - accountType: protoAction.AccountType, - source: source, - destination: destination, - }, nil -} - -func (h *CloseDormantAccountActionHandler) TransactionCount() int { - return 1 -} - -func (h *CloseDormantAccountActionHandler) PopulateMetadata(actionRecord *action.Record) error { - // All actions are revoked, except for those that perform the gift card auto-return - // - // Important Note: Given the critical implications of closing a dormant account, - // especially as an accident due to a bug, ensure proper safeguards in the - // fulfillment scheduler exist. See commented code in that file. - initialState := action.StateRevoked - if h.accountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - initialState = action.StateUnknown - } - - actionRecord.Source = h.source.Vault.PublicKey().ToBase58() - - destination := h.destination.PublicKey().ToBase58() - actionRecord.Destination = &destination - - // Do not populat a quantity. This will be done later when we decide to schedule - // the action. Otherwise, the balance calculator will be completely off. Also, it's - // not clear what the end balance will be, since this action is reserved for the - // future when the balance state will likely have changed. - actionRecord.Quantity = nil - - actionRecord.State = initialState - - return nil -} - -func (h *CloseDormantAccountActionHandler) GetServerParameter() *transactionpb.ServerParameter { - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_CloseDormantAccount{ - CloseDormantAccount: &transactionpb.CloseDormantAccountServerParameter{}, - }, - } -} - -func (h *CloseDormantAccountActionHandler) RequiresNonce(index int) bool { - return true -} - -func (h *CloseDormantAccountActionHandler) MakeNewSolanaTransaction( - index int, - nonce *common.Account, - bh solana.Blockhash, -) (*makeSolanaTransactionResult, error) { - switch index { - case 0: - txn, err := transaction_util.MakeCloseAccountWithBalanceTransaction( - nonce, - bh, - h.source, - h.destination, - nil, - ) - if err != nil { - return nil, err - } - - intentOrderingIndex := uint64(math.MaxInt64) - actionOrderingIndex := uint32(0) - - return &makeSolanaTransactionResult{ - txn: &txn, - - fulfillmentType: fulfillment.CloseDormantTimelockAccount, - source: h.source.Vault, - destination: h.destination, - intentOrderingIndexOverride: &intentOrderingIndex, - actionOrderingIndexOverride: &actionOrderingIndex, - fulfillmentOrderingIndex: 0, - disableActiveScheduling: true, - }, nil - default: - return nil, errors.New("invalid transaction index") - } -} - -func (h *CloseDormantAccountActionHandler) OnSaveToDB(ctx context.Context) error { - return nil -} - type NoPrivacyTransferActionHandler struct { source *common.TimelockAccounts destination *common.Account @@ -431,7 +229,7 @@ func NewNoPrivacyTransferActionHandler(protoAction *transactionpb.NoPrivacyTrans return nil, err } - source, err := sourceAuthority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } @@ -455,7 +253,7 @@ func NewFeePaymentActionHandler(protoAction *transactionpb.FeePaymentAction, fee return nil, err } - source, err := sourceAuthority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } @@ -481,7 +279,7 @@ func NewFeePaymentActionHandler(protoAction *transactionpb.FeePaymentAction, fee }, nil } -func (h *NoPrivacyTransferActionHandler) TransactionCount() int { +func (h *NoPrivacyTransferActionHandler) FulfillmentCount() int { return 1 } @@ -524,14 +322,14 @@ func (h *NoPrivacyTransferActionHandler) RequiresNonce(index int) bool { return true } -func (h *NoPrivacyTransferActionHandler) MakeNewSolanaTransaction( +func (h *NoPrivacyTransferActionHandler) GetFulfillmentMetadata( index int, nonce *common.Account, bh solana.Blockhash, -) (*makeSolanaTransactionResult, error) { +) (*newFulfillmentMetadata, error) { switch index { case 0: - txn, err := transaction_util.MakeTransferWithAuthorityTransaction( + virtualIxnHash, err := transaction_util.GetVirtualTransferWithAuthorityHash( nonce, bh, h.source, @@ -542,8 +340,10 @@ func (h *NoPrivacyTransferActionHandler) MakeNewSolanaTransaction( return nil, err } - return &makeSolanaTransactionResult{ - txn: &txn, + return &newFulfillmentMetadata{ + requiresClientSignature: true, + expectedSigner: h.source.VaultOwner, + virtualIxnHash: virtualIxnHash, fulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, source: h.source.Vault, @@ -564,30 +364,17 @@ type NoPrivacyWithdrawActionHandler struct { source *common.TimelockAccounts destination *common.Account amount uint64 - additionalMemo *string disableActiveScheduling bool } func NewNoPrivacyWithdrawActionHandler(intentRecord *intent.Record, protoAction *transactionpb.NoPrivacyWithdrawAction) (CreateActionHandler, error) { - dataVersion := timelock_token_v1.DataVersion1 - var additionalMemo *string var disableActiveScheduling bool switch intentRecord.IntentType { case intent.SendPrivatePayment: - if intentRecord.SendPrivatePaymentMetadata.IsTip { - tipMemo, err := transaction_util.GetTipMemoValue(intentRecord.SendPrivatePaymentMetadata.TipMetadata.Platform, intentRecord.SendPrivatePaymentMetadata.TipMetadata.Username) - if err != nil { - return nil, err - } - additionalMemo = &tipMemo - } - // Technically we should do this for public receives too, but we don't // yet have a great way of doing cross intent fulfillment polling hints. disableActiveScheduling = true - case intent.MigrateToPrivacy2022: - dataVersion = timelock_token_v1.DataVersionLegacy } sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) @@ -595,7 +382,7 @@ func NewNoPrivacyWithdrawActionHandler(intentRecord *intent.Record, protoAction return nil, err } - source, err := sourceAuthority.GetTimelockAccounts(dataVersion, common.KinMintAccount) + source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } @@ -609,12 +396,11 @@ func NewNoPrivacyWithdrawActionHandler(intentRecord *intent.Record, protoAction source: source, destination: destination, amount: protoAction.Amount, - additionalMemo: additionalMemo, disableActiveScheduling: disableActiveScheduling, }, nil } -func (h *NoPrivacyWithdrawActionHandler) TransactionCount() int { +func (h *NoPrivacyWithdrawActionHandler) FulfillmentCount() int { return 1 } @@ -642,26 +428,27 @@ func (h *NoPrivacyWithdrawActionHandler) RequiresNonce(index int) bool { return true } -func (h *NoPrivacyWithdrawActionHandler) MakeNewSolanaTransaction( +func (h *NoPrivacyWithdrawActionHandler) GetFulfillmentMetadata( index int, nonce *common.Account, bh solana.Blockhash, -) (*makeSolanaTransactionResult, error) { +) (*newFulfillmentMetadata, error) { switch index { case 0: - txn, err := transaction_util.MakeCloseAccountWithBalanceTransaction( + virtualIxnHash, err := transaction_util.GetVirtualCloseAccountWithBalanceHash( nonce, bh, h.source, h.destination, - h.additionalMemo, ) if err != nil { return nil, err } - return &makeSolanaTransactionResult{ - txn: &txn, + return &newFulfillmentMetadata{ + requiresClientSignature: true, + expectedSigner: h.source.VaultOwner, + virtualIxnHash: virtualIxnHash, fulfillmentType: fulfillment.NoPrivacyWithdraw, source: h.source.Vault, @@ -753,7 +540,7 @@ func NewTemporaryPrivacyTransferActionHandler( return nil, err } - h.source, err = authority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + h.source, err = authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } @@ -789,10 +576,10 @@ func NewTemporaryPrivacyTransferActionHandler( amount, ) - commitmentAddress, commitmentBump, err := splitter_token.GetCommitmentStateAddress(&splitter_token.GetCommitmentStateAddressArgs{ - Pool: h.treasuryPool.PublicKey().ToBytes(), - RecentRoot: []byte(h.recentRoot), - Transcript: h.transcript, + commitmentAddress, _, err := cvm.GetRelayCommitmentAddress(&cvm.GetRelayCommitmentAddressArgs{ + Relay: h.treasuryPool.PublicKey().ToBytes(), + MerkleRoot: cvm.Hash(h.recentRoot), + Transcript: cvm.Hash(h.transcript), Destination: h.destination.PublicKey().ToBytes(), Amount: amount, }) @@ -804,9 +591,17 @@ func NewTemporaryPrivacyTransferActionHandler( return nil, err } - commitmentVaultAddress, commitmentVaultBump, err := splitter_token.GetCommitmentVaultAddress(&splitter_token.GetCommitmentVaultAddressArgs{ - Pool: h.treasuryPool.PublicKey().ToBytes(), - Commitment: h.commitment.PublicKey().ToBytes(), + proofAddress, _, err := cvm.GetRelayProofAddress(&cvm.GetRelayProofAddressArgs{ + Relay: h.treasuryPool.PublicKey().ToBytes(), + MerkleRoot: cvm.Hash(h.recentRoot), + Commitment: cvm.Hash(commitmentAddress), + }) + if err != nil { + return nil, err + } + + commitmentVaultAddress, _, err := cvm.GetRelayVaultAddress(&cvm.GetRelayVaultAddressArgs{ + RelayOrProof: proofAddress, }) if err != nil { return nil, err @@ -817,26 +612,20 @@ func NewTemporaryPrivacyTransferActionHandler( } h.unsavedCommitmentRecord = &commitment.Record{ - DataVersion: splitter_token.DataVersion1, - - Address: h.commitment.PublicKey().ToBase58(), - Bump: commitmentBump, - - Vault: h.commitmentVault.PublicKey().ToBase58(), - VaultBump: commitmentVaultBump, - - Pool: h.treasuryPool.PublicKey().ToBase58(), - PoolBump: cachedTreasuryMetadata.stateBump, + Address: h.commitment.PublicKey().ToBase58(), + VaultAddress: h.commitmentVault.PublicKey().ToBase58(), + Pool: h.treasuryPool.PublicKey().ToBase58(), RecentRoot: cachedTreasuryMetadata.mostRecentRoot, - Transcript: hex.EncodeToString(h.transcript), + Transcript: hex.EncodeToString(h.transcript), Destination: h.destination.PublicKey().ToBase58(), Amount: amount, Intent: intentRecord.IntentId, ActionId: untypedAction.Id, - Owner: intentRecord.InitiatorOwnerAccount, + + Owner: intentRecord.InitiatorOwnerAccount, State: commitment.StateUnknown, } @@ -844,7 +633,7 @@ func NewTemporaryPrivacyTransferActionHandler( return h, nil } -func (h *TemporaryPrivacyTransferActionHandler) TransactionCount() int { +func (h *TemporaryPrivacyTransferActionHandler) FulfillmentCount() int { return 2 } @@ -888,16 +677,17 @@ func (h *TemporaryPrivacyTransferActionHandler) RequiresNonce(index int) bool { return index != 0 } -func (h *TemporaryPrivacyTransferActionHandler) MakeNewSolanaTransaction( +func (h *TemporaryPrivacyTransferActionHandler) GetFulfillmentMetadata( index int, nonce *common.Account, bh solana.Blockhash, -) (*makeSolanaTransactionResult, error) { +) (*newFulfillmentMetadata, error) { switch index { case 0: - return &makeSolanaTransactionResult{ - isCreatedOnDemand: true, - txn: nil, + return &newFulfillmentMetadata{ + requiresClientSignature: false, + expectedSigner: nil, + virtualIxnHash: nil, fulfillmentType: fulfillment.TransferWithCommitment, source: h.treasuryPoolVault, @@ -906,7 +696,7 @@ func (h *TemporaryPrivacyTransferActionHandler) MakeNewSolanaTransaction( disableActiveScheduling: h.isCollectedForHideInTheCrowdPrivacy, }, nil case 1: - txn, err := transaction_util.MakeTransferWithAuthorityTransaction( + virtualIxnHash, err := transaction_util.GetVirtualTransferWithAuthorityHash( nonce, bh, h.source, @@ -917,8 +707,10 @@ func (h *TemporaryPrivacyTransferActionHandler) MakeNewSolanaTransaction( return nil, err } - return &makeSolanaTransactionResult{ - txn: &txn, + return &newFulfillmentMetadata{ + requiresClientSignature: true, + expectedSigner: h.source.VaultOwner, + virtualIxnHash: virtualIxnHash, fulfillmentType: fulfillment.TemporaryPrivacyTransferWithAuthority, source: h.source.Vault, @@ -935,6 +727,7 @@ func (h *TemporaryPrivacyTransferActionHandler) OnSaveToDB(ctx context.Context) return h.data.SaveCommitment(ctx, h.unsavedCommitmentRecord) } +/* // Handles both of the equivalent client transfer and exchange actions. The // server-defined action only defines the private movement of funds between // accounts and it's all treated the same by backend processes. The client @@ -1083,6 +876,7 @@ func (h *PermanentPrivacyUpgradeActionHandler) OnSaveToDB(ctx context.Context) e return h.data.SaveCommitment(ctx, commitmentBeingUpgraded) } +*/ func getTransript( intent string, diff --git a/pkg/code/server/grpc/transaction/v2/airdrop.go b/pkg/code/server/grpc/transaction/v2/airdrop.go index 0eca26fb..5b12d88d 100644 --- a/pkg/code/server/grpc/transaction/v2/airdrop.go +++ b/pkg/code/server/grpc/transaction/v2/airdrop.go @@ -2,41 +2,12 @@ package transaction_v2 import ( "context" - "crypto/sha256" - "database/sql" - "fmt" - "time" - "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" "github.com/code-payments/code-server/pkg/cache" - "github.com/code-payments/code-server/pkg/code/balance" - chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/event" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/nonce" - event_util "github.com/code-payments/code-server/pkg/code/event" - exchange_rate_util "github.com/code-payments/code-server/pkg/code/exchangerate" - push_util "github.com/code-payments/code-server/pkg/code/push" - "github.com/code-payments/code-server/pkg/code/transaction" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) // This is a quick and dirty file to get an initial airdrop feature out @@ -67,6 +38,8 @@ var ( cachedFirstReceivesByOwner = cache.NewCache(10_000) ) +/* + func (s *transactionServer) Airdrop(ctx context.Context, req *transactionpb.AirdropRequest) (*transactionpb.AirdropResponse, error) { log := s.log.WithFields(logrus.Fields{ "method": "Airdrop", @@ -179,172 +152,6 @@ func (s *transactionServer) Airdrop(ctx context.Context, req *transactionpb.Aird }, nil } -func (s *transactionServer) maybeAirdropForSubmittingIntent(ctx context.Context, intentRecord *intent.Record, submitActionsOwnerMetadata *common.OwnerMetadata) { - if false { - // Disabled - s.maybeAirdropForSendingUserTheirFirstKin(ctx, intentRecord) - } -} - -func (s *transactionServer) maybeAirdropForSendingUserTheirFirstKin(ctx context.Context, intentRecord *intent.Record) error { - log := s.log.WithFields(logrus.Fields{ - "method": "maybeAirdropForSendingUserTheirFirstKin", - "intent": intentRecord.IntentId, - "intent_type": intentRecord.IntentType, - }) - - var ownerToAirdrop *common.Account - var ownerToCheckForFirstReceive *common.Account - var quarksGivenByReferrer uint64 - var exchangedIn currency_lib.Code - var nativeAmount float64 - var err error - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - // Not a direct payment to a Code user - if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) == 0 { - return nil - } - - // Private movement of funds within the same owner - if intentRecord.InitiatorOwnerAccount == intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount { - return nil - } - - ownerToAirdrop, err = common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - log.WithError(err).Warn("failure getting owner to airdrop") - return err - } - - ownerToCheckForFirstReceive, err = common.NewAccountFromPublicKeyString(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) - if err != nil { - log.WithError(err).Warn("failure getting owner to check for first receive") - return err - } - - quarksGivenByReferrer = intentRecord.SendPrivatePaymentMetadata.Quantity - exchangedIn = intentRecord.SendPrivatePaymentMetadata.ExchangeCurrency - nativeAmount = intentRecord.SendPrivatePaymentMetadata.NativeAmount - case intent.SendPublicPayment: - // Not a direct payment to a Code user - if len(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) == 0 { - return nil - } - - // Public movement of funds within the same owner - if intentRecord.InitiatorOwnerAccount == intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount { - return nil - } - - ownerToAirdrop, err = common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - log.WithError(err).Warn("failure getting owner to airdrop") - return err - } - - ownerToCheckForFirstReceive, err = common.NewAccountFromPublicKeyString(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) - if err != nil { - log.WithError(err).Warn("failure getting owner to check for first receive") - return err - } - - quarksGivenByReferrer = intentRecord.SendPublicPaymentMetadata.Quantity - exchangedIn = intentRecord.SendPublicPaymentMetadata.ExchangeCurrency - nativeAmount = intentRecord.SendPublicPaymentMetadata.NativeAmount - case intent.ReceivePaymentsPublicly: - // Not receiving a gift card - if !intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend { - return nil - } - - // Gift card is being voided - if intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard { - return nil - } - - giftCardIssuedIntentRecord, err := s.data.GetOriginalGiftCardIssuedIntent(ctx, intentRecord.ReceivePaymentsPubliclyMetadata.Source) - if err != nil { - log.WithError(err).Warn("failure getting gift card issued intent") - return err - } - - // The same user is claiming their gift card - if giftCardIssuedIntentRecord.InitiatorOwnerAccount == intentRecord.InitiatorOwnerAccount { - return nil - } - - ownerToAirdrop, err = common.NewAccountFromPublicKeyString(giftCardIssuedIntentRecord.InitiatorOwnerAccount) - if err != nil { - log.WithError(err).Warn("failure getting owner to airdrop") - return err - } - - ownerToCheckForFirstReceive, err = common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - log.WithError(err).Warn("failure getting owner to check for first receive") - return err - } - - quarksGivenByReferrer = intentRecord.ReceivePaymentsPubliclyMetadata.Quantity - exchangedIn = giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeCurrency - nativeAmount = giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.NativeAmount - default: - return nil - } - - log = log.WithFields(logrus.Fields{ - "owner_to_airdrop": ownerToAirdrop.PublicKey().ToBase58(), - "owner_to_check_for_first_receive": ownerToCheckForFirstReceive.PublicKey().ToBase58(), - }) - - for _, owner := range []*common.Account{ownerToAirdrop, ownerToCheckForFirstReceive} { - isEligible, err := s.data.IsEligibleForAirdrop(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting airdrop eligibility for owner account") - return err - } - if !isEligible { - return nil - } - } - - isFirstReceive, err := s.isFirstReceiveFromOtherCodeUser(ctx, intentRecord.IntentId, ownerToCheckForFirstReceive) - if err != nil { - log.WithError(err).Warn("failure checking if intent is user's first receive from someone else") - return err - } else if !isFirstReceive { - return nil - } - - if !s.conf.disableAntispamChecks.Get(ctx) { - allow, err := s.antispamGuard.AllowReferralBonus( - ctx, - ownerToAirdrop, - ownerToCheckForFirstReceive, - s.airdropper.VaultOwner, - quarksGivenByReferrer, - exchangedIn, - nativeAmount, - ) - if err != nil { - log.WithError(err).Warn("failure performing antispam check") - return err - } else if !allow { - return nil - } - } - - intentId := GetNewAirdropIntentId(AirdropTypeGiveFirstKin, intentRecord.IntentId) - _, err = s.airdrop(ctx, intentId, ownerToAirdrop, AirdropTypeGiveFirstKin) - if err != nil { - log.Warn("failure giving airdrop") - return err - } - - return nil -} - // airdrop gives Kin airdrops denominated in USD for performing certain // actions in the Code app. This funciton is idempotent with the given // intent ID. @@ -730,6 +537,23 @@ func (s *transactionServer) isFirstReceiveFromOtherCodeUser(ctx context.Context, return firstReceive.IntentId == intentToCheck, nil } +func GetOldAirdropIntentId(airdropType AirdropType, reference string) string { + return fmt.Sprintf("airdrop-%d-%s", airdropType, reference) +} + +// Consistent intent ID that maps to a 32 byte buffer +func GetNewAirdropIntentId(airdropType AirdropType, reference string) string { + old := GetOldAirdropIntentId(airdropType, reference) + hashed := sha256.Sum256([]byte(old)) + return base58.Encode(hashed[:]) +} + +func getAirdropCacheKey(owner *common.Account, airdropType AirdropType) string { + return fmt.Sprintf("%s:%d\n", owner.PublicKey().ToBase58(), airdropType) +} + +*/ + func (s *transactionServer) mustLoadAirdropper(ctx context.Context) { log := s.log.WithFields(logrus.Fields{ "method": "mustLoadAirdropper", @@ -747,7 +571,7 @@ func (s *transactionServer) mustLoadAirdropper(ctx context.Context) { return err } - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return err } @@ -760,21 +584,6 @@ func (s *transactionServer) mustLoadAirdropper(ctx context.Context) { } } -func GetOldAirdropIntentId(airdropType AirdropType, reference string) string { - return fmt.Sprintf("airdrop-%d-%s", airdropType, reference) -} - -// Consistent intent ID that maps to a 32 byte buffer -func GetNewAirdropIntentId(airdropType AirdropType, reference string) string { - old := GetOldAirdropIntentId(airdropType, reference) - hashed := sha256.Sum256([]byte(old)) - return base58.Encode(hashed[:]) -} - -func getAirdropCacheKey(owner *common.Account, airdropType AirdropType) string { - return fmt.Sprintf("%s:%d\n", owner.PublicKey().ToBase58(), airdropType) -} - func (t AirdropType) String() string { switch t { case AirdropTypeUnknown: diff --git a/pkg/code/server/grpc/transaction/v2/airdrop_test.go b/pkg/code/server/grpc/transaction/v2/airdrop_test.go index f754735a..2b531cb6 100644 --- a/pkg/code/server/grpc/transaction/v2/airdrop_test.go +++ b/pkg/code/server/grpc/transaction/v2/airdrop_test.go @@ -1,5 +1,6 @@ package transaction_v2 +/* import ( "testing" @@ -76,7 +77,7 @@ func TestAirdrop_GetFirstKin_InsufficientBalance(t *testing.T) { server.assertNotAirdroppedFirstKin(t, phone) } -/* + func TestAirdrop_GiveFirstKin_CodeToCodePayment(t *testing.T) { for _, clearCache := range []bool{true, false} { server, sendingPhone, receivingPhone, cleanup := setupTestEnv(t, &testOverrides{ @@ -364,7 +365,7 @@ func TestAirdrop_GiveFirstKin_InsufficientAirdropperFunds(t *testing.T) { submitIntentCall.requireSuccess(t) server.assertNotAirdroppedForGivingFirstKin(t, submitIntentCall.intentId) } -*/ + func TestAirdrop_IntentId(t *testing.T) { reference1 := testutil.NewRandomAccount(t).PublicKey().ToBase58() @@ -392,3 +393,4 @@ func TestAirdrop_IntentId(t *testing.T) { assert.Equal(t, generated3, GetNewAirdropIntentId(AirdropTypeGetFirstKin, reference2)) } } +*/ diff --git a/pkg/code/server/grpc/transaction/v2/errors.go b/pkg/code/server/grpc/transaction/v2/errors.go index d806a3f6..354bcd19 100644 --- a/pkg/code/server/grpc/transaction/v2/errors.go +++ b/pkg/code/server/grpc/transaction/v2/errors.go @@ -14,6 +14,7 @@ import ( "github.com/code-payments/code-server/pkg/code/antispam" "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" ) const ( @@ -173,7 +174,7 @@ func toReasonStringErrorDetails(err error) *transactionpb.ErrorDetails { } } -func toInvalidSignatureErrorDetails( +func toInvalidTxnSignatureErrorDetails( actionId uint32, txn solana.Transaction, signature *commonpb.Signature, @@ -188,8 +189,30 @@ func toInvalidSignatureErrorDetails( Type: &transactionpb.ErrorDetails_InvalidSignature{ InvalidSignature: &transactionpb.InvalidSignatureErrorDetails{ ActionId: actionId, - ExpectedTransaction: &commonpb.Transaction{ - Value: txn.Marshal(), + ExpectedBlob: &transactionpb.InvalidSignatureErrorDetails_ExpectedTransaction{ + ExpectedTransaction: &commonpb.Transaction{ + Value: txn.Marshal(), + }, + }, + ProvidedSignature: signature, + }, + }, + } +} + +func toInvalidVirtualIxnSignatureErrorDetails( + actionId uint32, + virtualIxnHash cvm.Hash, + signature *commonpb.Signature, +) *transactionpb.ErrorDetails { + return &transactionpb.ErrorDetails{ + Type: &transactionpb.ErrorDetails_InvalidSignature{ + InvalidSignature: &transactionpb.InvalidSignatureErrorDetails{ + ActionId: actionId, + ExpectedBlob: &transactionpb.InvalidSignatureErrorDetails_ExpectedVixnHash{ + ExpectedVixnHash: &commonpb.Hash{ + Value: virtualIxnHash[:], + }, }, ProvidedSignature: signature, }, diff --git a/pkg/code/server/grpc/transaction/v2/history.go b/pkg/code/server/grpc/transaction/v2/history.go deleted file mode 100644 index 02e52058..00000000 --- a/pkg/code/server/grpc/transaction/v2/history.go +++ /dev/null @@ -1,289 +0,0 @@ -package transaction_v2 - -import ( - "context" - "math" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/timestamppb" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/kin" -) - -const maxHistoryPageSize = 100 - -func (s *transactionServer) GetPaymentHistory(ctx context.Context, req *transactionpb.GetPaymentHistoryRequest) (*transactionpb.GetPaymentHistoryResponse, error) { - log := s.log.WithField("method", "GetPaymentHistory") - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - // Get the number of results that should be returned - var limit uint64 - if req.PageSize > 0 { - limit = uint64(req.PageSize) - } else { - limit = maxHistoryPageSize - } - - // Enforce proto limit - if limit > maxHistoryPageSize { - limit = maxHistoryPageSize - } - - // Convert the proto ordering type to the internal type - var direction query.Ordering - if req.Direction == transactionpb.GetPaymentHistoryRequest_ASC { - direction = query.Ascending - } else { - direction = query.Descending - } - - // Convert the proto cursor type to our native uint64 cursor (ID) - var cursor query.Cursor - if req.Cursor != nil { - cursor = req.Cursor.Value - } else { - cursor = query.ToCursor(0) - if direction == query.Descending { - cursor = query.ToCursor(math.MaxInt64 - 1) - } - } - - // Get all intents for the owner as both a source and destination - intentRecords, err := s.data.GetAllIntentsByOwner( - ctx, - owner.PublicKey().ToBase58(), - query.WithLimit(limit), - query.WithDirection(direction), - query.WithCursor(cursor), - ) - if err == intent.ErrIntentNotFound { - return &transactionpb.GetPaymentHistoryResponse{ - Result: transactionpb.GetPaymentHistoryResponse_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure querying intent records") - return nil, status.Error(codes.Internal, "") - } - - var items []*transactionpb.PaymentHistoryItem - for _, intentRecord := range intentRecords { - var paymentType transactionpb.PaymentHistoryItem_PaymentType - var isDeposit, isWithdrawal, isRemoteSend, isReturned, isAirdrop, isMicroPayment bool - var exchangeData *transactionpb.ExchangeData - var airdropType transactionpb.AirdropType - - intentId, err := common.NewAccountFromPublicKeyString(intentRecord.IntentId) - if err != nil { - // Some legacy intent IDs are not public keys, so generate a random one. - // This is fine since this RPC is deprecated, and we are only concerned - // about support use cases for newly created payments. - intentId, err = common.NewRandomAccount() - if err != nil { - log.WithError(err).Warn("failure generating intent id") - return nil, status.Error(codes.Internal, "") - } - } - - // Extract payment details from intents, where applicable, into a view - // that makes sense for a user. - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - paymentType = transactionpb.PaymentHistoryItem_SEND - isRemoteSend = intentRecord.SendPrivatePaymentMetadata.IsRemoteSend - isWithdrawal = intentRecord.SendPrivatePaymentMetadata.IsWithdrawal - isDeposit = false - isMicroPayment = intentRecord.SendPrivatePaymentMetadata.IsMicroPayment - if intentRecord.InitiatorOwnerAccount != owner.PublicKey().ToBase58() { - paymentType = transactionpb.PaymentHistoryItem_RECEIVE - isWithdrawal = false - isDeposit = intentRecord.SendPrivatePaymentMetadata.IsWithdrawal - } - - // Default to a receive if this is a micro payment within the same owner - if isMicroPayment && intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount == owner.PublicKey().ToBase58() { - paymentType = transactionpb.PaymentHistoryItem_RECEIVE - isWithdrawal = false - isDeposit = intentRecord.SendPrivatePaymentMetadata.IsWithdrawal - } - - // Funds moving within the same owner don't get populated when they're - // used to support another payment flow that represents the history item - // (eg. public withdrawals with private top ups) - if isWithdrawal && intentRecord.InitiatorOwnerAccount == intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount && !isMicroPayment { - continue - } - - // Don't show history items where the user voids the gift card. - if isRemoteSend { - claimedIntent, err := s.data.GetGiftCardClaimedIntent(ctx, intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount) - if err == nil && claimedIntent.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard { - continue - } else if err != nil && err != intent.ErrIntentNotFound { - log.WithError(err).Warn("failure getting gift card claimed intent") - return nil, status.Error(codes.Internal, "") - } - } - - exchangeData = &transactionpb.ExchangeData{ - Currency: string(intentRecord.SendPrivatePaymentMetadata.ExchangeCurrency), - ExchangeRate: intentRecord.SendPrivatePaymentMetadata.ExchangeRate, - NativeAmount: intentRecord.SendPrivatePaymentMetadata.NativeAmount, - Quarks: intentRecord.SendPrivatePaymentMetadata.Quantity, - } - case intent.SendPublicPayment: - paymentType = transactionpb.PaymentHistoryItem_SEND - isRemoteSend = false - isWithdrawal = intentRecord.SendPublicPaymentMetadata.IsWithdrawal - isDeposit = false - isMicroPayment = false - if intentRecord.InitiatorOwnerAccount != owner.PublicKey().ToBase58() { - paymentType = transactionpb.PaymentHistoryItem_RECEIVE - isWithdrawal = false - isDeposit = intentRecord.SendPublicPaymentMetadata.IsWithdrawal - } - - // Bonus airdrops only occur within Code->Code withdrawal flows - if s.airdropper != nil { - isAirdrop = (intentRecord.InitiatorOwnerAccount == s.airdropper.VaultOwner.PublicKey().ToBase58()) - } - - if isAirdrop { - // todo: something less hacky - if intentRecord.SendPublicPaymentMetadata.NativeAmount == 5.0 { - airdropType = transactionpb.AirdropType_GIVE_FIRST_KIN - } else if intentRecord.SendPublicPaymentMetadata.NativeAmount == 1.0 { - airdropType = transactionpb.AirdropType_GET_FIRST_KIN - } - } - - exchangeData = &transactionpb.ExchangeData{ - Currency: string(intentRecord.SendPublicPaymentMetadata.ExchangeCurrency), - ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate, - NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount, - Quarks: intentRecord.SendPublicPaymentMetadata.Quantity, - } - case intent.ReceivePaymentsPrivately: - // Other intents account for history items - continue - case intent.MigrateToPrivacy2022: - // Don't show migrations for dust - if intentRecord.MigrateToPrivacy2022Metadata.Quantity < kin.ToQuarks(1) { - continue - } - - paymentType = transactionpb.PaymentHistoryItem_RECEIVE - isDeposit = true - exchangeData = &transactionpb.ExchangeData{ - Currency: string(currency.KIN), - ExchangeRate: 1.0, - NativeAmount: float64(intentRecord.MigrateToPrivacy2022Metadata.Quantity) / kin.QuarksPerKin, - Quarks: intentRecord.MigrateToPrivacy2022Metadata.Quantity, - } - case intent.ExternalDeposit: - if intentRecord.ExternalDepositMetadata.DestinationOwnerAccount != owner.PublicKey().ToBase58() { - continue - } - - // Don't show deposits for dust - if intentRecord.ExternalDepositMetadata.Quantity < kin.ToQuarks(1) { - continue - } - - paymentType = transactionpb.PaymentHistoryItem_RECEIVE - isDeposit = true - exchangeData = &transactionpb.ExchangeData{ - Currency: string(currency.KIN), - ExchangeRate: 1.0, - NativeAmount: float64(intentRecord.ExternalDepositMetadata.Quantity) / kin.QuarksPerKin, - Quarks: intentRecord.ExternalDepositMetadata.Quantity, - } - case intent.ReceivePaymentsPublicly: - // The intent to create the remote send gift card has no knowledge of the - // destination owner since it's claimed at a later time, so the history item - // must come from the intent receiving it. - if !intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend { - continue - } - - // Don't show history items where the user voids the gift card. - if intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard { - continue - } - - paymentType = transactionpb.PaymentHistoryItem_RECEIVE - isRemoteSend = intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend - isReturned = intentRecord.ReceivePaymentsPubliclyMetadata.IsReturned - isWithdrawal = false - isDeposit = false - - exchangeData = &transactionpb.ExchangeData{ - Currency: string(intentRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeCurrency), - ExchangeRate: intentRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate, - NativeAmount: intentRecord.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount, - Quarks: intentRecord.ReceivePaymentsPubliclyMetadata.Quantity, - } - default: - continue - } - - item := &transactionpb.PaymentHistoryItem{ - Cursor: &transactionpb.Cursor{ - Value: query.ToCursor(intentRecord.Id), - }, - - ExchangeData: exchangeData, - - PaymentType: paymentType, - - IsWithdraw: isWithdrawal, - IsDeposit: isDeposit, - IsRemoteSend: isRemoteSend, - IsReturned: isReturned, - IsAirdrop: isAirdrop, - IsMicroPayment: isMicroPayment, - - AirdropType: airdropType, - - IntentId: &commonpb.IntentId{ - Value: intentId.PublicKey().ToBytes(), - }, - - Timestamp: timestamppb.New(intentRecord.CreatedAt), - } - - items = append(items, item) - } - - if len(items) == 0 { - return &transactionpb.GetPaymentHistoryResponse{ - Result: transactionpb.GetPaymentHistoryResponse_NOT_FOUND, - }, nil - } - - return &transactionpb.GetPaymentHistoryResponse{ - Result: transactionpb.GetPaymentHistoryResponse_OK, - Items: items, - }, nil -} diff --git a/pkg/code/server/grpc/transaction/v2/history_test.go b/pkg/code/server/grpc/transaction/v2/history_test.go deleted file mode 100644 index f2cddee5..00000000 --- a/pkg/code/server/grpc/transaction/v2/history_test.go +++ /dev/null @@ -1,424 +0,0 @@ -package transaction_v2 - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - chat_util "github.com/code-payments/code-server/pkg/code/chat" - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/chat" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" - "github.com/code-payments/code-server/pkg/testutil" -) - -func TestPaymentHistory_HappyPath(t *testing.T) { - server, sendingPhone, receivingPhone, cleanup := setupTestEnv(t, &testOverrides{ - enableAirdrops: true, - }) - defer cleanup() - - merchantDomain := "example.com" - twitterUsername := "tipthisuser" - - server.generateAvailableNonces(t, 1000) - server.setupAirdropper(t, kin.ToQuarks(1_500_000_000)) - server.simulateTwitterRegistration(t, twitterUsername, receivingPhone.getTimelockVault(t, commonpb.AccountType_PRIMARY, 0)) - - amountForPrivacyMigration := kin.ToQuarks(23) - legacyTimelockVault, err := sendingPhone.parentAccount.ToTimelockVault(timelock_token_v1.DataVersionLegacy, common.KinMintAccount) - require.NoError(t, err) - server.fundAccount(t, legacyTimelockVault, amountForPrivacyMigration) - - sendingPhone.openAccounts(t).requireSuccess(t) - receivingPhone.openAccounts(t).requireSuccess(t) - sendingPhone.establishRelationshipWithMerchant(t, merchantDomain).requireSuccess(t) - receivingPhone.establishRelationshipWithMerchant(t, merchantDomain).requireSuccess(t) - - server.fundAccount(t, getTimelockVault(t, sendingPhone.getAuthorityForRelationshipAccount(t, merchantDomain)), kin.ToQuarks(1_000_000)) - - // [Cash Transactions] sendingPhone DEPOSITED 23 Kin - sendingPhone.migrateToPrivacy2022(t, amountForPrivacyMigration).requireSuccess(t) - - receivingPhone.deposit777KinIntoOrganizer(t).requireSuccess(t) - - // [Cash Transactions] sendingPhone GAVE $4.20 USD of Kin - // [Cash Transactions] receivingPhone RECEIVED $4.20 USD of Kin - sendingPhone.send42KinToCodeUser(t, receivingPhone).requireSuccess(t) - receivingPhone.receive42KinFromCodeUser(t).requireSuccess(t) - - // [Cash Transactions] sendingPhone WITHDREW $77.70 USD of Kin - // [Cash Transactions] receivingPhone DEPOSITED $77.70 USD of Kin - sendingPhone.privatelyWithdraw777KinToCodeUser(t, receivingPhone).requireSuccess(t) - - // [Cash Transactions] sendingPhone WITHDREW 123 Kin - sendingPhone.privatelyWithdraw123KinToExternalWallet(t).requireSuccess(t) - - // [Cash Transactions] receivingPhone DEPOSITED 10,000,000,000 Kin - server.simulateExternalDepositHistoryItem(t, receivingPhone.parentAccount, receivingPhone.getTimelockVault(t, commonpb.AccountType_PRIMARY, 0), kin.ToQuarks(10_000_000_000)) - receivingPhone.deposit777KinIntoOrganizer(t).requireSuccess(t) - - // [Cash Transactions] sendingPhone WITHDREW $77.70 USD of Kin - // [Cash Transactions] receivingPhone DEPOSITED $77.70 USD of Kin - sendingPhone.privatelyWithdraw777KinToCodeUser(t, sendingPhone).requireSuccess(t) - sendingPhone.publiclyWithdraw777KinToCodeUserBetweenPrimaryAccounts(t, receivingPhone).requireSuccess(t) - - // [Cash Transactions] sendingPhone SENT $2.10 CAD of Kin - // [Cash Transactions] receivingPhone RECEIVED $2.10 CAD of Kin - happyPathGiftCardAccount := testutil.NewRandomAccount(t) - sendingPhone.send42KinToGiftCardAccount(t, happyPathGiftCardAccount).requireSuccess(t) - receivingPhone.receive42KinFromGiftCard(t, happyPathGiftCardAccount, false).requireSuccess(t) - receivingPhone.receive42KinPrivatelyIntoOrganizer(t).requireSuccess(t) - - // [Cash Transactions] sendingPhone SENT $2.10 CAD of Kin - expiredGiftCardAccount := testutil.NewRandomAccount(t) - sendingPhone.send42KinToGiftCardAccount(t, expiredGiftCardAccount).requireSuccess(t) - server.simulateExpiredGiftCard(t, expiredGiftCardAccount) - - // [Cash Transactions] sendingPhone SENT $2.10 CAD of Kin - unclaimedGiftCardAccount := testutil.NewRandomAccount(t) - sendingPhone.send42KinToGiftCardAccount(t, unclaimedGiftCardAccount).requireSuccess(t) - - // [Cash Transactions] receivingPhone SENT $2.10 CAD of Kin - // [Cash Transactions] receivingPhone RECEIVED $2.10 CAD of Kin - selfClaimedGiftCardAccount := testutil.NewRandomAccount(t) - receivingPhone.send42KinToGiftCardAccount(t, selfClaimedGiftCardAccount).requireSuccess(t) - receivingPhone.receive42KinFromGiftCard(t, selfClaimedGiftCardAccount, false).requireSuccess(t) - receivingPhone.receive42KinPrivatelyIntoOrganizer(t).requireSuccess(t) - - voidedGiftCardAccount := testutil.NewRandomAccount(t) - sendingPhone.send42KinToGiftCardAccount(t, voidedGiftCardAccount).requireSuccess(t) - sendingPhone.receive42KinFromGiftCard(t, voidedGiftCardAccount, true).requireSuccess(t) - sendingPhone.receive42KinPrivatelyIntoOrganizer(t).requireSuccess(t) - - assert.Equal(t, transactionpb.AirdropResponse_OK, receivingPhone.requestAirdrop(t, transactionpb.AirdropType_GET_FIRST_KIN).Result) - - sendingPhone.resetConfig() - sendingPhone.conf.simulatePaymentRequest = true - - // [Verified Merchant] sendingPhone SPENT $77.7 USD of Kin - // [Verified Merchant] receivingPhone RECEIVED $77.69 USD of Kin - sendingPhone.privatelyWithdraw777KinToCodeUser(t, receivingPhone).requireSuccess(t) - receivingPhone.deposit777KinIntoOrganizer(t).requireSuccess(t) - - sendingPhone.resetConfig() - sendingPhone.conf.simulatePaymentRequest = true - sendingPhone.conf.simulateAdditionalFees = true - - // [Verified Merchant] sendingPhone SPENT $32.1 USD of Kin - // [Verified Merchant] receivingPhone RECEIVED $29.69213 USD of Kin - sendingPhone.privatelyWithdraw321KinToCodeUserRelationshipAccount(t, receivingPhone, merchantDomain).requireSuccess(t) - - // [Verified Merchant] sendingPhone SPENT 123 Kin - sendingPhone.privatelyWithdraw123KinToExternalWallet(t).requireSuccess(t) - - receivingPhone.resetConfig() - receivingPhone.conf.simulatePaymentRequest = true - receivingPhone.conf.simulateUnverifiedPaymentRequest = true - - // [Unverified Mechant] receivingPhone RECEIVED $77.69 USD of Kin - receivingPhone.privatelyWithdraw777KinToCodeUser(t, receivingPhone).requireSuccess(t) - - sendingPhone.resetConfig() - receivingPhone.resetConfig() - - // [Cash Transactions] sendingPhone WITHDREW $32.1 USD of Kin - // [Verified Merchant] receivingPhone RECEIVED $32.1 USD of Kin - sendingPhone.publiclyWithdraw777KinToCodeUserBetweenRelationshipAccounts(t, merchantDomain, receivingPhone).requireSuccess(t) - - // [Cash Transactions] sendingPhone WITHDREW $32.1 USD of Kin - // [Verified Merchant] receivingPhone RECEIVED $32.1 USD of Kin - sendingPhone.privatelyWithdraw321KinToCodeUserRelationshipAccount(t, receivingPhone, merchantDomain).requireSuccess(t) - - // [Verified Merchant] receivingPhone RECEIVED 12,345 Kin - server.simulateExternalDepositHistoryItem(t, receivingPhone.parentAccount, getTimelockVault(t, receivingPhone.getAuthorityForRelationshipAccount(t, merchantDomain)), kin.ToQuarks(12_345)) - - sendingPhone.tip456KinToCodeUser(t, receivingPhone, twitterUsername).requireSuccess(t) - - chatMessageRecords, err := server.data.GetAllChatMessages(server.ctx, chat.GetChatId(chat_util.CashTransactionsName, sendingPhone.parentAccount.PublicKey().ToBase58(), true)) - require.NoError(t, err) - require.Len(t, chatMessageRecords, 10) - - protoChatMessage := getProtoChatMessage(t, chatMessageRecords[0]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_DEPOSITED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.KIN, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 1.0, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 23.0, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(23), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[1]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_GAVE, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 4.20, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[2]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_WITHDREW, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.7, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(777), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[3]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_WITHDREW, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.KIN, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 1.0, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 123.0, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(123), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[4]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_WITHDREW, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.7, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(777), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[5]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_SENT, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.CAD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.05, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 2.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[6]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_SENT, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.CAD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.05, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 2.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[7]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_SENT, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.CAD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.05, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 2.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[8]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_WITHDREW, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.7, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(777), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[9]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_WITHDREW, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 32.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(321), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId("example.com", sendingPhone.parentAccount.PublicKey().ToBase58(), true)) - require.NoError(t, err) - require.Len(t, chatMessageRecords, 3) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[0]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_SPENT, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.7, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(777), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[1]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_SPENT, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 32.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(321), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[2]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_SPENT, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.KIN, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 1.0, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 123.0, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(123), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId(chat_util.CashTransactionsName, receivingPhone.parentAccount.PublicKey().ToBase58(), true)) - require.NoError(t, err) - require.Len(t, chatMessageRecords, 7) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[0]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 4.20, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[1]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_DEPOSITED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.7, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(777), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[2]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_DEPOSITED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.KIN, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 1.0, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 10_000_000_000.00, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(10_000_000_000), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[3]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_DEPOSITED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.7, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(777), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[4]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.CAD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.05, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 2.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[5]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_SENT, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.CAD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.05, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 2.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[6]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.CAD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.05, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 2.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId(chat_util.TipsName, sendingPhone.parentAccount.PublicKey().ToBase58(), true)) - require.NoError(t, err) - require.Len(t, chatMessageRecords, 1) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[0]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_SENT_TIP, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 45.6, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(456), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId("example.com", receivingPhone.parentAccount.PublicKey().ToBase58(), true)) - require.NoError(t, err) - require.Len(t, chatMessageRecords, 5) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[0]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.69, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.EqualValues(t, 77690000, protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[1]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 29.69213, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.EqualValues(t, 29692130, protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[2]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.7, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(777), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[3]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 32.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(321), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[4]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.KIN, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 1.0, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 12_345.0, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(12_345), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId("example.com", receivingPhone.parentAccount.PublicKey().ToBase58(), false)) - require.NoError(t, err) - require.Len(t, chatMessageRecords, 1) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[0]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 77.69, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.EqualValues(t, 77690000, protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId(chat_util.TipsName, receivingPhone.parentAccount.PublicKey().ToBase58(), true)) - require.NoError(t, err) - require.Len(t, chatMessageRecords, 1) - - protoChatMessage = getProtoChatMessage(t, chatMessageRecords[0]) - require.Len(t, protoChatMessage.Content, 1) - require.NotNil(t, protoChatMessage.Content[0].GetExchangeData()) - assert.Equal(t, chatpb.ExchangeDataContent_RECEIVED_TIP, protoChatMessage.Content[0].GetExchangeData().Verb) - assert.EqualValues(t, currency_lib.USD, protoChatMessage.Content[0].GetExchangeData().GetExact().Currency) - assert.Equal(t, 0.1, protoChatMessage.Content[0].GetExchangeData().GetExact().ExchangeRate) - assert.Equal(t, 45.6, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) - assert.Equal(t, kin.ToQuarks(456), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) -} diff --git a/pkg/code/server/grpc/transaction/v2/intent.go b/pkg/code/server/grpc/transaction/v2/intent.go index f6d2c786..77e100fd 100644 --- a/pkg/code/server/grpc/transaction/v2/intent.go +++ b/pkg/code/server/grpc/transaction/v2/intent.go @@ -6,7 +6,6 @@ import ( "crypto/ed25519" "database/sql" "encoding/base64" - "encoding/hex" "strings" "time" @@ -24,10 +23,8 @@ import ( chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/nonce" @@ -40,16 +37,10 @@ import ( "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/solana" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" + "github.com/code-payments/code-server/pkg/solana/cvm" "github.com/code-payments/code-server/pkg/solana/token" ) -const ( - // Assumes the client signature index is consistent across all transactions, - // including those constructed in the SubmitIntent and Swap RPCs. - clientSignatureIndex = 1 -) - func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_SubmitIntentServer) error { // Bound the total RPC. Keeping the timeout higher to see where we land because // there's a lot of stuff happening in this method. @@ -124,12 +115,11 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm log = log.WithField("intent_type", "receive_payments_privately") intentHandler = NewReceivePaymentsPrivatelyIntentHandler(s.conf, s.data, s.antispamGuard, s.amlGuard) intentRequiresNewTreasuryPoolFunds = true - case *transactionpb.Metadata_UpgradePrivacy: - log = log.WithField("intent_type", "upgrade_privacy") - intentHandler = NewUpgradePrivacyIntentHandler(s.conf, s.data) - case *transactionpb.Metadata_MigrateToPrivacy_2022: - log = log.WithField("intent_type", "migrate_to_privacy_2022") - intentHandler = NewMigrateToPrivacy2022IntentHandler(s.conf, s.data) + /* + case *transactionpb.Metadata_UpgradePrivacy: + log = log.WithField("intent_type", "upgrade_privacy") + intentHandler = NewUpgradePrivacyIntentHandler(s.conf, s.data) + */ case *transactionpb.Metadata_SendPublicPayment: log = log.WithField("intent_type", "send_public_payment") intentHandler = NewSendPublicPaymentIntentHandler(s.conf, s.data, s.pusher, s.antispamGuard, s.maxmind) @@ -139,6 +129,7 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm case *transactionpb.Metadata_EstablishRelationship: log = log.WithField("intent_type", "establish_relationship") intentHandler = NewEstablishRelationshipIntentHandler(s.conf, s.data, s.antispamGuard) + default: return handleSubmitIntentError(streamer, status.Error(codes.InvalidArgument, "SubmitIntentRequest.SubmitActions.Metadata is nil")) } @@ -410,22 +401,18 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm phoneLock.Unlock() phoneLockUnlocked = true - type fulfillmentWithMetadata struct { - record *fulfillment.Record - isRecordSaved bool - - txn *solana.Transaction - isCreatedOnDemand bool + type fulfillmentWithSigningMetadata struct { + record *fulfillment.Record requiresClientSignature bool - - intentOrderingIndexOverriden bool + expectedSigner *common.Account + virtualIxnHash *cvm.Hash } // Convert all actions into a set of fulfillments var actionHandlers []BaseActionHandler var actionRecords []*action.Record - var fulfillments []fulfillmentWithMetadata + var fulfillments []fulfillmentWithSigningMetadata var reservedNonces []*transaction.SelectedNonce var serverParameters []*transactionpb.ServerParameter for i, protoAction := range submitActionsReq.Actions { @@ -440,14 +427,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm log = log.WithField("action_type", "open_account") actionType = action.OpenAccount actionHandler, err = NewOpenAccountActionHandler(s.data, typed.OpenAccount, submitActionsReq.Metadata) - case *transactionpb.Action_CloseEmptyAccount: - log = log.WithField("action_type", "close_empty_account") - actionType = action.CloseEmptyAccount - actionHandler, err = NewCloseEmptyAccountActionHandler(intentRecord.IntentType, typed.CloseEmptyAccount) - case *transactionpb.Action_CloseDormantAccount: - log = log.WithField("action_type", "close_dormant_account") - actionType = action.CloseDormantAccount - actionHandler, err = NewCloseDormantAccountActionHandler(typed.CloseDormantAccount) case *transactionpb.Action_NoPrivacyTransfer: log = log.WithField("action_type", "no_privacy_transfer") actionType = action.NoPrivacyTransfer @@ -468,25 +447,27 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm log = log.WithField("action_type", "temporary_privacy_exchange") actionType = action.PrivateTransfer actionHandler, err = NewTemporaryPrivacyTransferActionHandler(ctx, s.conf, s.data, intentRecord, protoAction, true, s.selectTreasuryPoolForAdvance) - case *transactionpb.Action_PermanentPrivacyUpgrade: - log = log.WithField("action_type", "permanent_privacy_upgrade") - actionType = action.PrivateTransfer - - // Pass along the privacy upgrade target found during intent validation - // to avoid duplication of work. - cachedUpgradeTarget, ok := intentHandler.(*UpgradePrivacyIntentHandler).GetCachedUpgradeTarget(typed.PermanentPrivacyUpgrade) - if !ok { - log.Warn("cached privacy upgrade target not found") - return handleSubmitIntentError(streamer, errors.New("cached privacy upgrade target not found")) - } + /* + case *transactionpb.Action_PermanentPrivacyUpgrade: + log = log.WithField("action_type", "permanent_privacy_upgrade") + actionType = action.PrivateTransfer + + // Pass along the privacy upgrade target found during intent validation + // to avoid duplication of work. + cachedUpgradeTarget, ok := intentHandler.(*UpgradePrivacyIntentHandler).GetCachedUpgradeTarget(typed.PermanentPrivacyUpgrade) + if !ok { + log.Warn("cached privacy upgrade target not found") + return handleSubmitIntentError(streamer, errors.New("cached privacy upgrade target not found")) + } - actionHandler, err = NewPermanentPrivacyUpgradeActionHandler( - ctx, - s.data, - intentRecord, - typed.PermanentPrivacyUpgrade, - cachedUpgradeTarget, - ) + actionHandler, err = NewPermanentPrivacyUpgradeActionHandler( + ctx, + s.data, + intentRecord, + typed.PermanentPrivacyUpgrade, + cachedUpgradeTarget, + ) + */ default: return handleSubmitIntentError(streamer, status.Errorf(codes.InvalidArgument, "SubmitIntentRequest.SubmitActions.Actions[%d].Type is nil", i)) } @@ -510,14 +491,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm actionHandlers = append(actionHandlers, actionHandler) - // Some actions are optional tools for Code to use at its disposal and - // are not necessarily required to complete the intent flow. We can - // choose to discard them by revoking the action immediately. However, - // clients still have an expectation to sign them, since this is a - // server toggle. As a result, we must go through the process of creating - // the transaction. - areFulfillmentsSavedForAction := true - // Upgrades cannot create new actions. if !isUpgradeActionOperation { // Construct the equivalent action record @@ -539,9 +512,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm return handleSubmitIntentError(streamer, err) } - // Action could be immediately revoked if we opt to not require it - areFulfillmentsSavedForAction = actionRecord.State != action.StateRevoked - actionRecords = append(actionRecords, actionRecord) } @@ -550,13 +520,13 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm serverParameter.ActionId = protoAction.Id serverParameters = append(serverParameters, serverParameter) - transactionCount := 1 + fulfillmentCount := 1 if !isUpgradeActionOperation { - transactionCount = actionHandler.(CreateActionHandler).TransactionCount() + fulfillmentCount = actionHandler.(CreateActionHandler).FulfillmentCount() } - for j := 0; j < transactionCount; j++ { - var makeTxnResult *makeSolanaTransactionResult + for j := 0; j < fulfillmentCount; j++ { + var newFulfillmentMetadata *newFulfillmentMetadata var selectedNonce *transaction.SelectedNonce var actionId uint32 if isUpgradeActionOperation { @@ -571,6 +541,8 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm // Re-use the same nonce as the one in the fulfillment we're upgrading, // so we avoid server from submitting both. + // + // todo: This doesn't select the virtual durable nonce selectedNonce, err = transaction.SelectNonceFromFulfillmentToUpgrade(ctx, s.data, fulfillmentToUpgrade) if err != nil { log.WithError(err).Warn("failure selecting nonce from existing fulfillment") @@ -578,13 +550,13 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm } defer selectedNonce.Unlock() - // Make a new transaction, which is the upgraded version of the old one. - makeTxnResult, err = upgradeActionHandler.MakeUpgradedSolanaTransaction( + // Get metadata for the fulfillment being upgraded + newFulfillmentMetadata, err = upgradeActionHandler.GetFulfillmentMetadata( selectedNonce.Account, selectedNonce.Blockhash, ) if err != nil { - log.WithError(err).Warn("failure making solana transaction") + log.WithError(err).Warn("failure getting fulfillment metadata") return handleSubmitIntentError(streamer, err) } @@ -597,7 +569,7 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm var nonceAccount *common.Account var nonceBlockchash solana.Blockhash if createActionHandler.RequiresNonce(j) { - selectedNonce, err = transaction.SelectAvailableNonce(ctx, s.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) + selectedNonce, err = transaction.SelectAvailableNonce(ctx, s.data, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), nonce.PurposeClientTransaction) if err != nil { log.WithError(err).Warn("failure selecting available nonce") return handleSubmitIntentError(streamer, err) @@ -616,29 +588,20 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm selectedNonce = nil } - // Make a new transaction - makeTxnResult, err = createActionHandler.MakeNewSolanaTransaction( + // Get metadata for the new fulfillment being created + newFulfillmentMetadata, err = createActionHandler.GetFulfillmentMetadata( j, nonceAccount, nonceBlockchash, ) if err != nil { - log.WithError(err).Warn("failure making solana transaction") + log.WithError(err).Warn("failure getting fulfillment metadata") return handleSubmitIntentError(streamer, err) } actionId = protoAction.Id } - // Sign the Solana transaction - if !makeTxnResult.isCreatedOnDemand { - err = makeTxnResult.txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) - if err != nil { - log.WithError(err).Warn("failure signing solana transaction") - return handleSubmitIntentError(streamer, err) - } - } - // Construct the fulfillment record fulfillmentRecord := &fulfillment.Record{ Intent: intentRecord.IntentId, @@ -647,66 +610,43 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm ActionId: actionId, ActionType: actionType, - FulfillmentType: makeTxnResult.fulfillmentType, + FulfillmentType: newFulfillmentMetadata.fulfillmentType, - Source: makeTxnResult.source.PublicKey().ToBase58(), + Source: newFulfillmentMetadata.source.PublicKey().ToBase58(), IntentOrderingIndex: 0, // Unknown until intent record is saved, so it's injected later ActionOrderingIndex: actionId, - FulfillmentOrderingIndex: makeTxnResult.fulfillmentOrderingIndex, + FulfillmentOrderingIndex: newFulfillmentMetadata.fulfillmentOrderingIndex, - DisableActiveScheduling: makeTxnResult.disableActiveScheduling, + DisableActiveScheduling: newFulfillmentMetadata.disableActiveScheduling, InitiatorPhoneNumber: intentRecord.InitiatorPhoneNumber, State: fulfillment.StateUnknown, } - if !makeTxnResult.isCreatedOnDemand { - fulfillmentRecord.Data = makeTxnResult.txn.Marshal() - fulfillmentRecord.Signature = pointer.String(base58.Encode(makeTxnResult.txn.Signature())) - - fulfillmentRecord.Nonce = pointer.String(selectedNonce.Account.PublicKey().ToBase58()) - fulfillmentRecord.Blockhash = pointer.String(base58.Encode(selectedNonce.Blockhash[:])) - } - if makeTxnResult.destination != nil { - destination := makeTxnResult.destination.PublicKey().ToBase58() - fulfillmentRecord.Destination = &destination - } - if makeTxnResult.intentOrderingIndexOverride != nil { - fulfillmentRecord.IntentOrderingIndex = *makeTxnResult.intentOrderingIndexOverride - } - if makeTxnResult.actionOrderingIndexOverride != nil { - fulfillmentRecord.ActionOrderingIndex = *makeTxnResult.actionOrderingIndexOverride + if newFulfillmentMetadata.destination != nil { + fulfillmentRecord.Destination = pointer.String(newFulfillmentMetadata.destination.PublicKey().ToBase58()) } - // Transaction requires a client signature - var requiresClientSignature bool - if !makeTxnResult.isCreatedOnDemand && makeTxnResult.txn.Message.Header.NumSignatures > clientSignatureIndex { - // Upgraded transactions always use the same nonce, so there's no - // need to provide it. - if !isUpgradeActionOperation { - serverParameter.Nonces = append(serverParameter.Nonces, &transactionpb.NoncedTransactionMetadata{ - Nonce: selectedNonce.Account.ToProto(), - Blockhash: &commonpb.Blockhash{ - Value: selectedNonce.Blockhash[:], - }, - }) - } + // Fulfillment has a virtual instruction requiring client signature + if newFulfillmentMetadata.requiresClientSignature { + fulfillmentRecord.VirtualNonce = pointer.String(selectedNonce.Account.PublicKey().ToBase58()) + fulfillmentRecord.VirtualBlockhash = pointer.String(base58.Encode(selectedNonce.Blockhash[:])) - requiresClientSignature = true + serverParameter.Nonces = append(serverParameter.Nonces, &transactionpb.NoncedTransactionMetadata{ + Nonce: selectedNonce.Account.ToProto(), + Blockhash: &commonpb.Blockhash{ + Value: selectedNonce.Blockhash[:], + }, + }) } - fulfillments = append(fulfillments, fulfillmentWithMetadata{ + fulfillments = append(fulfillments, fulfillmentWithSigningMetadata{ record: fulfillmentRecord, - isCreatedOnDemand: makeTxnResult.isCreatedOnDemand, - txn: makeTxnResult.txn, - - requiresClientSignature: requiresClientSignature, - - intentOrderingIndexOverriden: makeTxnResult.intentOrderingIndexOverride != nil, - - isRecordSaved: areFulfillmentsSavedForAction, + requiresClientSignature: newFulfillmentMetadata.requiresClientSignature, + expectedSigner: newFulfillmentMetadata.expectedSigner, + virtualIxnHash: newFulfillmentMetadata.virtualIxnHash, }) reservedNonces = append(reservedNonces, selectedNonce) } @@ -720,7 +660,7 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm }, } - var unsignedFulfillments []fulfillmentWithMetadata + var unsignedFulfillments []fulfillmentWithSigningMetadata for _, fulfillmentWithMetadata := range fulfillments { if fulfillmentWithMetadata.requiresClientSignature { unsignedFulfillments = append(unsignedFulfillments, fulfillmentWithMetadata) @@ -780,15 +720,14 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm unsignedFulfillment := unsignedFulfillments[i] if !ed25519.Verify( - unsignedFulfillment.txn.Message.Accounts[clientSignatureIndex], - unsignedFulfillment.txn.Message.Marshal(), + unsignedFulfillment.expectedSigner.PublicKey().ToBytes(), + unsignedFulfillment.virtualIxnHash[:], signature.Value, ) { - signatureErrorDetails = append(signatureErrorDetails, toInvalidSignatureErrorDetails(unsignedFulfillment.record.ActionId, *unsignedFulfillment.txn, signature)) + signatureErrorDetails = append(signatureErrorDetails, toInvalidVirtualIxnSignatureErrorDetails(unsignedFulfillment.record.ActionId, *unsignedFulfillment.virtualIxnHash, signature)) } - copy(unsignedFulfillment.txn.Signatures[clientSignatureIndex][:], signature.Value) - unsignedFulfillments[i].record.Data = unsignedFulfillments[i].txn.Marshal() + unsignedFulfillment.record.VirtualSignature = pointer.String(base58.Encode(signature.Value)) } if len(signatureErrorDetails) > 0 { @@ -829,21 +768,12 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm // Save all fulfillment records fulfillmentRecordsToSave := make([]*fulfillment.Record, 0) for i, fulfillmentWithMetadata := range fulfillments { - if !fulfillmentWithMetadata.isRecordSaved { - continue - } - - // If the intent ordering index isn't overriden, the inject it here where - // the value is guaranteed to be set due to the lazy saving of the intent - // record. - if !fulfillmentWithMetadata.intentOrderingIndexOverriden { - fulfillmentWithMetadata.record.IntentOrderingIndex = intentRecord.Id - } + fulfillmentWithMetadata.record.IntentOrderingIndex = intentRecord.Id fulfillmentRecordsToSave = append(fulfillmentRecordsToSave, fulfillmentWithMetadata.record) // Reserve the nonce with the latest server-signed fulfillment. - if !fulfillmentWithMetadata.isCreatedOnDemand { + if fulfillmentWithMetadata.requiresClientSignature { nonceToReserve := reservedNonces[i] if isIntentUpdateOperation { err = nonceToReserve.UpdateSignature(ctx, *fulfillmentWithMetadata.record.Signature) @@ -1008,15 +938,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm if ok { backgroundCtx = context.WithValue(backgroundCtx, metrics.NewRelicContextKey, nr) } - - // todo: We likely want to put this in a worker if this is a long term feature - if s.conf.enableAirdrops.Get(backgroundCtx) { - if s.conf.enableAsyncAirdropProcessing.Get(backgroundCtx) { - go s.maybeAirdropForSubmittingIntent(backgroundCtx, intentRecord, submitActionsOwnerMetadata) - } else { - s.maybeAirdropForSubmittingIntent(backgroundCtx, intentRecord, submitActionsOwnerMetadata) - } - } } // RPC is finished. Send success to the client @@ -1178,14 +1099,6 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact }, }, } - case intent.MigrateToPrivacy2022: - metadata = &transactionpb.Metadata{ - Type: &transactionpb.Metadata_MigrateToPrivacy_2022{ - MigrateToPrivacy_2022: &transactionpb.MigrateToPrivacy2022Metadata{ - Quarks: intentRecord.MigrateToPrivacy2022Metadata.Quantity, - }, - }, - } case intent.SendPublicPayment: destinationAccount, err := common.NewAccountFromPublicKeyString(intentRecord.SendPublicPaymentMetadata.DestinationTokenAccount) if err != nil { @@ -1265,12 +1178,6 @@ func (s *transactionServer) CanWithdrawToAccount(ctx context.Context, req *trans timelockRecord, err := s.data.GetTimelockByVault(ctx, accountToCheck.PublicKey().ToBase58()) switch err { case nil: - if timelockRecord.DataVersion != timelock_token_v1.DataVersion1 { - return &transactionpb.CanWithdrawToAccountResponse{ - IsValidPaymentDestination: false, - AccountType: transactionpb.CanWithdrawToAccountResponse_TokenAccount, - }, nil - } case timelock.ErrTimelockNotFound: // Nothing to do default: @@ -1350,6 +1257,7 @@ func (s *transactionServer) CanWithdrawToAccount(ctx context.Context, req *trans }, nil } +/* func (s *transactionServer) GetPrivacyUpgradeStatus(ctx context.Context, req *transactionpb.GetPrivacyUpgradeStatusRequest) (*transactionpb.GetPrivacyUpgradeStatusResponse, error) { intentId := base58.Encode(req.IntentId.Value) @@ -1374,7 +1282,7 @@ func (s *transactionServer) GetPrivacyUpgradeStatus(ctx context.Context, req *tr case ErrInvalidActionToUpgrade: result = transactionpb.GetPrivacyUpgradeStatusResponse_INVALID_ACTION case ErrPrivacyUpgradeMissed: - upgradeStatus = transactionpb.GetPrivacyUpgradeStatusResponse_TEMPORARY_TRANSACTION_FINALIZED + upgradeStatus = transactionpb.GetPrivacyUpgradeStatusResponse_TEMPORARY_ACTION_FINALIZED case ErrPrivacyAlreadyUpgraded: upgradeStatus = transactionpb.GetPrivacyUpgradeStatusResponse_ALREADY_UPGRADED case ErrWaitForNextBlock: @@ -1571,3 +1479,4 @@ func toUpgradeableIntentProto(ctx context.Context, data code_data.Provider, inte Actions: actions, }, nil } +*/ diff --git a/pkg/code/server/grpc/transaction/v2/intent_handler.go b/pkg/code/server/grpc/transaction/v2/intent_handler.go index b54cb7b8..baaf645c 100644 --- a/pkg/code/server/grpc/transaction/v2/intent_handler.go +++ b/pkg/code/server/grpc/transaction/v2/intent_handler.go @@ -23,7 +23,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/event" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/paymentrequest" - "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/code/data/twitter" event_util "github.com/code-payments/code-server/pkg/code/event" exchange_rate_util "github.com/code-payments/code-server/pkg/code/exchangerate" @@ -34,7 +33,6 @@ import ( "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" push_lib "github.com/code-payments/code-server/pkg/push" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) // todo: Make working with different timelock versions easier @@ -244,55 +242,13 @@ func (h *OpenAccountsIntentHandler) AllowCreation(ctx context.Context, intentRec } func (h *OpenAccountsIntentHandler) validateActions(initiatiorOwnerAccount *common.Account, actions []*transactionpb.Action) error { - // Validate action count. Primary account has 1 open action and every other - // account has 1 open and 1 close action. - expectedActionCount := 2*len(accountTypesToOpen) - 1 + expectedActionCount := len(accountTypesToOpen) if len(actions) != expectedActionCount { return newIntentValidationErrorf("expected %d total actions", expectedActionCount) } - // Validate the first action is opening the primary account using the owner - // account as the authority. - - openAction := actions[0] - if openAction.GetOpenAccount() == nil { - return newActionValidationError(openAction, "expected an open account action") - } - - if openAction.GetOpenAccount().AccountType != commonpb.AccountType_PRIMARY { - return newActionValidationErrorf(openAction, "account type must be %s", commonpb.AccountType_PRIMARY) - } - - if openAction.GetOpenAccount().Index != 0 { - return newActionValidationError(openAction, "index must be 0 for all newly opened accounts") - } - - if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatiorOwnerAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "owner must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) - } - - if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(openAction, "authority must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) - } - - expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) - if err != nil { - return err - } - - if !bytes.Equal(openAction.GetOpenAccount().Token.Value, expectedVaultAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) - } - - tokenAccountToCollapseTo := openAction.GetOpenAccount().Token - - // Validate all other actions are to open and close all other remaining account types - actions = actions[1:] - for i, expectedAccountType := range accountTypesToOpen[1:] { - openAction := actions[2*i] - closeDormantAction := actions[2*i+1] - - // Validate the open action + for i, expectedAccountType := range accountTypesToOpen { + openAction := actions[i] if openAction.GetOpenAccount() == nil { return newActionValidationError(openAction, "expected an open account action") @@ -310,8 +266,15 @@ func (h *OpenAccountsIntentHandler) validateActions(initiatiorOwnerAccount *comm return newActionValidationErrorf(openAction, "owner must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) } - if bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(openAction, "authority cannot be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) + switch expectedAccountType { + case commonpb.AccountType_PRIMARY: + if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { + return newActionValidationErrorf(openAction, "authority must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) + } + default: + if bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { + return newActionValidationErrorf(openAction, "authority cannot be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) + } } expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) @@ -322,28 +285,6 @@ func (h *OpenAccountsIntentHandler) validateActions(initiatiorOwnerAccount *comm if !bytes.Equal(openAction.GetOpenAccount().Token.Value, expectedVaultAccount.PublicKey().ToBytes()) { return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) } - - // Validate the close dormant action - - if closeDormantAction.GetCloseDormantAccount() == nil { - return newActionValidationError(closeDormantAction, "expected a close dormant account action") - } - - if closeDormantAction.GetCloseDormantAccount().AccountType != expectedAccountType { - return newActionValidationErrorf(closeDormantAction, "expected %s account type", expectedAccountType) - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Authority.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(closeDormantAction, "authority must be %s", base58.Encode(openAction.GetOpenAccount().Authority.Value)) - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Token.Value, openAction.GetOpenAccount().Token.Value) { - return newActionValidationErrorf(closeDormantAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Destination.Value, tokenAccountToCollapseTo.Value) { - return newActionValidationErrorf(closeDormantAction, "destination must be %s", base58.Encode(tokenAccountToCollapseTo.Value)) - } } return nil @@ -529,6 +470,15 @@ func (h *SendPrivatePaymentIntentHandler) AllowCreation(ctx context.Context, int return errors.New("unexpected metadata proto message") } + // todo: need a solution for auto returns + if intentRecord.SendPrivatePaymentMetadata.IsRemoteSend { + return newIntentDeniedError("remote send is not supported yet for the vm") + } + // todo: need a solution for additional memo containing tipping platform and username in a memo + if intentRecord.SendPrivatePaymentMetadata.IsTip { + return newIntentDeniedError("tipping is not supported yet for the vm") + } + initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) if err != nil { return err @@ -835,19 +785,6 @@ func (h *SendPrivatePaymentIntentHandler) validateActions( return err } - // Ensure the client isn't trying to sneak in additional close dormant account actions - if metadata.IsRemoteSend { - err = validateCloseDormantAccountActionCount(actions, 2) - if err != nil { - return err - } - } else { - err = validateCloseDormantAccountActionCount(actions, 1) - if err != nil { - return err - } - } - // There's one closed account, and it must be the latest temporary outgoing account. closedAccounts := simResult.GetClosedAccounts() if len(closedAccounts) != 1 { @@ -1226,11 +1163,6 @@ func (h *ReceivePaymentsPrivatelyIntentHandler) validateActions( if len(closedAccounts) > 0 { return newActionValidationError(closedAccounts[0].CloseAction, "cannot close any account") } - - err = validateCloseDormantAccountActionCount(actions, 0) - if err != nil { - return err - } } else { // There's one opened account, and it must be the new temporary incoming account. @@ -1248,18 +1180,11 @@ func (h *ReceivePaymentsPrivatelyIntentHandler) validateActions( return err } - // Ensure the client isn't trying to sneak in additional close dormant account actions - err = validateCloseDormantAccountActionCount(actions, 1) - if err != nil { - return err - } - - // There's one closed account, and it must be the latest temporary incoming account. + // No accounts are closed, the latest temporary incoming account is simply + // compressed after used. closedAccounts := simResult.GetClosedAccounts() - if len(closedAccounts) != 1 { - return newIntentValidationError("must close one account") - } else if closedAccounts[0].TokenAccount.PublicKey().ToBase58() != source.PublicKey().ToBase58() { - return newActionValidationError(closedAccounts[0].CloseAction, "must close latest temporary incoming account") + if len(closedAccounts) != 0 { + return newIntentValidationError("cannot close any account") } } @@ -1377,6 +1302,7 @@ func (h *ReceivePaymentsPrivatelyIntentHandler) OnCommittedToDB(ctx context.Cont return nil } +/* type UpgradePrivacyIntentHandler struct { conf *conf data code_data.Provider @@ -1430,223 +1356,7 @@ func (h *UpgradePrivacyIntentHandler) GetCachedUpgradeTarget(protoAction *transa upgradeTo, ok := h.cachedUpgradeTargets[protoAction.ActionId] return upgradeTo, ok } - -type MigrateToPrivacy2022IntentHandler struct { - conf *conf - data code_data.Provider -} - -func NewMigrateToPrivacy2022IntentHandler(conf *conf, data code_data.Provider) CreateIntentHandler { - return &MigrateToPrivacy2022IntentHandler{ - conf: conf, - data: data, - } -} - -func (h *MigrateToPrivacy2022IntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { - typedProtoMetadata := protoMetadata.GetMigrateToPrivacy_2022() - if typedProtoMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - intentRecord.IntentType = intent.MigrateToPrivacy2022 - intentRecord.MigrateToPrivacy2022Metadata = &intent.MigrateToPrivacy2022Metadata{ - Quantity: typedProtoMetadata.Quarks, - } - - return nil -} - -func (h *MigrateToPrivacy2022IntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { - return false, nil -} - -func (h *MigrateToPrivacy2022IntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { - return &lockableAccounts{}, nil -} - -// Note: Most validation helper functions (eg. LocalSimulation) assume DataVersion1 -// timelock accounts and aren't used. That should be fine given there's only one action and -// validation is trivial. Migration of old timelock accounts is completely scoped to this -// intent type. -func (h *MigrateToPrivacy2022IntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action, deviceToken *string) error { - typedMetadata := metadata.GetMigrateToPrivacy_2022() - if typedMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - return err - } - - // - // Part 1: Intent ID validation - // - - err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) - if err != nil { - return err - } - - // - // Part 2: Validate there's a legacy timelock account to migrate. - // - - legacyTimelockAccounts, err := initiatiorOwnerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, common.KinMintAccount) - if err != nil { - return err - } - - legacyTimelockRecord, err := legacyTimelockAccounts.GetDBRecord(ctx, h.data) - if err == timelock.ErrTimelockNotFound { - return newStaleStateError("no account to migrate") - } else if err != nil { - return err - } - - // - // Part 3: Validate a privacy migration intent hasn't already been submitted - // - - _, err = h.data.GetLatestIntentByInitiatorAndType(ctx, intent.MigrateToPrivacy2022, initiatiorOwnerAccount.PublicKey().ToBase58()) - if err == nil { - return newStaleStateError("already submitted intent to migrate to privacy") - } else if err != intent.ErrIntentNotFound { - return err - } - - // - // Part 4: Validate an OpenAccounts intent has been submitted. In particular, - // we need the new primary account to exist. - // - - initiatorAccountsByType, err := common.GetLatestCodeTimelockAccountRecordsForOwner(ctx, h.data, initiatiorOwnerAccount) - if err != nil { - return err - } else if _, ok := initiatorAccountsByType[commonpb.AccountType_PRIMARY]; !ok { - return newStaleStateError("must submit open accounts intent") - } - - // - // Part 5: Validate all involved accounts are managed by code - // - - involvedAccounts := []*common.AccountRecords{ - initiatorAccountsByType[commonpb.AccountType_PRIMARY][0], - { - Timelock: legacyTimelockRecord, - General: nil, // validateAllAccountsManagedByCode requires common.AccountRecords, but this isn't used and doesn't exist in the DB - }, - } - err = validateAllUserAccountsManagedByCode(ctx, involvedAccounts) - if err != nil { - return err - } - - // - // Part 6: Validate full balance is being migrated - // - - balance, err := balance.CalculateFromCache(ctx, h.data, legacyTimelockAccounts.Vault) - if err != nil { - return err - } else if balance != typedMetadata.Quarks { - return newIntentValidationErrorf("must migrate %d quarks", balance) - } - - // - // Part 7: Validate the individual actions - // - - return h.validateActions(ctx, initiatiorOwnerAccount, typedMetadata, actions) -} - -func (h *MigrateToPrivacy2022IntentHandler) validateActions( - ctx context.Context, - initiatiorOwnerAccount *common.Account, - metadata *transactionpb.MigrateToPrivacy2022Metadata, - actions []*transactionpb.Action, -) error { - if len(actions) != 1 { - return newIntentValidationError("expected 1 action") - } - - legacyTimelockAccounts, err := initiatiorOwnerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, common.KinMintAccount) - if err != nil { - return err - } - - // - // Part 1: Validate actions match intent - // - - var authorityAccount, sourceAccount, destinationAccount *commonpb.SolanaAccountId - switch typed := actions[0].Type.(type) { - case *transactionpb.Action_NoPrivacyWithdraw: - if metadata.Quarks == 0 { - return newActionValidationError(actions[0], "expected a close empty account action") - } - - if typed.NoPrivacyWithdraw.Amount != metadata.Quarks { - return newActionValidationError(actions[0], "quark amount must match intent metadata") - } - - authorityAccount = typed.NoPrivacyWithdraw.Authority - sourceAccount = typed.NoPrivacyWithdraw.Source - destinationAccount = typed.NoPrivacyWithdraw.Destination - case *transactionpb.Action_CloseEmptyAccount: - if typed.CloseEmptyAccount.AccountType != commonpb.AccountType_LEGACY_PRIMARY_2022 { - return newActionValidationErrorf(actions[0], "account type must be %s", commonpb.AccountType_LEGACY_PRIMARY_2022) - } - - if metadata.Quarks > 0 { - return newActionValidationError(actions[0], "expected a no privacy withdraw action") - } - - authorityAccount = typed.CloseEmptyAccount.Authority - sourceAccount = typed.CloseEmptyAccount.Token - default: - if metadata.Quarks > 0 { - return newActionValidationError(actions[0], "expected a no privacy withdraw action") - } else { - return newActionValidationError(actions[0], "expected a close empty account action") - } - } - - // - // Part 2: Validate all accounts involved in the action - // - - if !bytes.Equal(authorityAccount.Value, initiatiorOwnerAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(actions[0], "authority must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) - } - - if !bytes.Equal(sourceAccount.Value, legacyTimelockAccounts.Vault.PublicKey().ToBytes()) { - return newActionValidationErrorf(actions[0], "must migrate from account %s", legacyTimelockAccounts.Vault.PublicKey().ToBase58()) - } - - if metadata.Quarks > 0 { - primaryTimelockAccounts, err := initiatiorOwnerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) - if err != nil { - return err - } - - if !bytes.Equal(destinationAccount.Value, primaryTimelockAccounts.Vault.PublicKey().ToBytes()) { - return newActionValidationErrorf(actions[0], "must migrate funds to account %s", primaryTimelockAccounts.Vault.PublicKey().ToBase58()) - } - } - - return nil -} - -func (h *MigrateToPrivacy2022IntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} - -func (h *MigrateToPrivacy2022IntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} +*/ type SendPublicPaymentIntentHandler struct { conf *conf @@ -2723,18 +2433,9 @@ func validateNextTemporaryAccountOpened( return errors.New("previous temp account record missing") } - primaryAccountRecords, ok := initiatorAccountsByType[commonpb.AccountType_PRIMARY] - if !ok { - return errors.New("primary account record missing") - } - tokenAccountToCollapseTo, err := common.NewAccountFromPublicKeyString(primaryAccountRecords[0].Timelock.VaultAddress) - if err != nil { - return err - } - // Find the open and close actions - var openAction, closeDormantAction *transactionpb.Action + var openAction *transactionpb.Action for _, action := range actions { switch typed := action.Type.(type) { case *transactionpb.Action_OpenAccount: @@ -2745,19 +2446,9 @@ func validateNextTemporaryAccountOpened( openAction = action } - case *transactionpb.Action_CloseDormantAccount: - if typed.CloseDormantAccount.AccountType == accountType { - if closeDormantAction != nil { - return newIntentValidationErrorf("multiple close dormant account actions for %s account type", accountType) - } - - closeDormantAction = action - } } } - // Validate the open action - if openAction == nil { return newIntentValidationErrorf("open account action for %s account type missing", accountType) } @@ -2784,28 +2475,6 @@ func validateNextTemporaryAccountOpened( return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) } - // Validate the close dormant action - - if closeDormantAction == nil { - return newIntentValidationErrorf("close dormant account action for %s account type missing", accountType) - } - - if closeDormantAction.Id < openAction.Id { - return newIntentValidationError("open account action must come before close dormant account action") - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Authority.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(closeDormantAction, "authority must be %s", base58.Encode(openAction.GetOpenAccount().Authority.Value)) - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Token.Value, openAction.GetOpenAccount().Token.Value) { - return newActionValidationErrorf(closeDormantAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Destination.Value, tokenAccountToCollapseTo.PublicKey().ToBytes()) { - return newActionValidationErrorf(closeDormantAction, "destination must be %s", tokenAccountToCollapseTo.PublicKey().ToBase58()) - } - return nil } @@ -2816,18 +2485,7 @@ func validateGiftCardAccountOpened( expectedGiftCardVault *common.Account, actions []*transactionpb.Action, ) error { - primaryAccountRecords, ok := initiatorAccountsByType[commonpb.AccountType_PRIMARY] - if !ok { - return errors.New("primary account record missing") - } - tokenAccountToCollapseTo, err := common.NewAccountFromPublicKeyString(primaryAccountRecords[0].Timelock.VaultAddress) - if err != nil { - return err - } - - // Find the open and close actions - - var openAction, closeDormantAction *transactionpb.Action + var openAction *transactionpb.Action for _, action := range actions { switch typed := action.Type.(type) { case *transactionpb.Action_OpenAccount: @@ -2838,19 +2496,9 @@ func validateGiftCardAccountOpened( openAction = action } - case *transactionpb.Action_CloseDormantAccount: - if typed.CloseDormantAccount.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - if closeDormantAction != nil { - return newIntentValidationErrorf("multiple close dormant account actions for %s account type", commonpb.AccountType_REMOTE_SEND_GIFT_CARD) - } - - closeDormantAction = action - } } } - // Validate the open action - if openAction == nil { return newIntentValidationErrorf("open account action for %s account type missing", commonpb.AccountType_REMOTE_SEND_GIFT_CARD) } @@ -2880,46 +2528,6 @@ func validateGiftCardAccountOpened( return newActionValidationErrorf(openAction, "token must be %s", derivedVaultAccount.PublicKey().ToBase58()) } - // Validate the close dormant action - - if closeDormantAction == nil { - return newIntentValidationErrorf("close dormant account action for %s account type missing", commonpb.AccountType_REMOTE_SEND_GIFT_CARD) - } - - if closeDormantAction.Id < openAction.Id { - return newIntentValidationError("open account action must come before close dormant account action") - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Authority.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(closeDormantAction, "authority must be %s", base58.Encode(openAction.GetOpenAccount().Authority.Value)) - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Token.Value, openAction.GetOpenAccount().Token.Value) { - return newActionValidationErrorf(closeDormantAction, "token must be %s", derivedVaultAccount.PublicKey().ToBase58()) - } - - if !bytes.Equal(closeDormantAction.GetCloseDormantAccount().Destination.Value, tokenAccountToCollapseTo.PublicKey().ToBytes()) { - return newActionValidationErrorf(closeDormantAction, "destination must be %s", tokenAccountToCollapseTo.PublicKey().ToBase58()) - } - - return nil -} - -func validateCloseDormantAccountActionCount(actions []*transactionpb.Action, expected int) error { - var actual int - for _, action := range actions { - switch action.Type.(type) { - case *transactionpb.Action_CloseDormantAccount: - actual++ - } - } - - if actual < expected { - return newIntentValidationError("too few close dormant account actions") - } - if actual > expected { - return newIntentValidationError("too many close dormant account actions") - } return nil } @@ -2935,13 +2543,16 @@ func validateNoUpgradeActions(actions []*transactionpb.Action) error { } func validateExternalKinTokenAccountWithinIntent(ctx context.Context, data code_data.Provider, tokenAccount *common.Account) error { - isValid, message, err := common.ValidateExternalKinTokenAccount(ctx, data, tokenAccount) - if err != nil { - return err - } else if !isValid { - return newIntentValidationError(message) - } - return nil + /* + isValid, message, err := common.ValidateExternalKinTokenAccount(ctx, data, tokenAccount) + if err != nil { + return err + } else if !isValid { + return newIntentValidationError(message) + } + return nil + */ + return newIntentDeniedError("external transfers are not yet supported") } func validateExchangeDataWithinIntent(ctx context.Context, data code_data.Provider, intentId string, proto *transactionpb.ExchangeData) error { @@ -3263,7 +2874,7 @@ func getExpectedTimelockVaultFromProtoAccount(authorityProto *commonpb.SolanaAcc return nil, err } - timelockAccounts, err := authorityAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } diff --git a/pkg/code/server/grpc/transaction/v2/intent_test.go b/pkg/code/server/grpc/transaction/v2/intent_test.go index cc8f6746..1b05d481 100644 --- a/pkg/code/server/grpc/transaction/v2/intent_test.go +++ b/pkg/code/server/grpc/transaction/v2/intent_test.go @@ -1,5 +1,6 @@ package transaction_v2 +/* import ( "encoding/hex" "fmt" @@ -3814,3 +3815,4 @@ func TestSubmitIntent_AdditionalAccountsToLock(t *testing.T) { assert.Nil(t, accountsToLock.DestinationOwner) assert.Nil(t, accountsToLock.RemoteSendGiftCardVault) } +*/ diff --git a/pkg/code/server/grpc/transaction/v2/limits_test.go b/pkg/code/server/grpc/transaction/v2/limits_test.go index 8ba92f31..be8e38a1 100644 --- a/pkg/code/server/grpc/transaction/v2/limits_test.go +++ b/pkg/code/server/grpc/transaction/v2/limits_test.go @@ -1,5 +1,6 @@ package transaction_v2 +/* import ( "crypto/ed25519" "testing" @@ -227,3 +228,4 @@ func TestGetLimits_UnauthenticateddAccess(t *testing.T) { _, err = phone.client.GetLimits(phone.ctx, req) testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) } +*/ diff --git a/pkg/code/server/grpc/transaction/v2/local_simulation.go b/pkg/code/server/grpc/transaction/v2/local_simulation.go index 621ed14e..1e4f5485 100644 --- a/pkg/code/server/grpc/transaction/v2/local_simulation.go +++ b/pkg/code/server/grpc/transaction/v2/local_simulation.go @@ -10,7 +10,6 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/timelock" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) type LocalSimulationResult struct { @@ -93,36 +92,6 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr OpenAction: action, }, ) - case *transactionpb.Action_CloseEmptyAccount: - closed, err := common.NewAccountFromProto(typedAction.CloseEmptyAccount.Token) - if err != nil { - return nil, err - } - derivedTimelockVault = closed - - authority, err = common.NewAccountFromProto(typedAction.CloseEmptyAccount.Authority) - if err != nil { - return nil, err - } - - simulations = append( - simulations, - TokenAccountSimulation{ - TokenAccount: closed, - Closed: true, - CloseAction: action, - }, - ) - case *transactionpb.Action_CloseDormantAccount: - derivedTimelockVault, err = common.NewAccountFromProto(typedAction.CloseDormantAccount.Token) - if err != nil { - return nil, err - } - - authority, err = common.NewAccountFromProto(typedAction.CloseDormantAccount.Authority) - if err != nil { - return nil, err - } case *transactionpb.Action_NoPrivacyTransfer: source, err := common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Source) if err != nil { @@ -352,7 +321,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr } // Validate authorities and respective derived timelock vault accounts match. - timelockAccounts, err := authority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } diff --git a/pkg/code/server/grpc/transaction/v2/local_simulation_test.go b/pkg/code/server/grpc/transaction/v2/local_simulation_test.go index 9a52d26b..18428fb1 100644 --- a/pkg/code/server/grpc/transaction/v2/local_simulation_test.go +++ b/pkg/code/server/grpc/transaction/v2/local_simulation_test.go @@ -1,5 +1,6 @@ package transaction_v2 +/* import ( "context" "fmt" @@ -841,3 +842,4 @@ func assertCorrectTransferSimulationFlags(t *testing.T, simulations []TransferSi } } } +*/ diff --git a/pkg/code/server/grpc/transaction/v2/onramp_test.go b/pkg/code/server/grpc/transaction/v2/onramp_test.go index 6339a65b..d5daf533 100644 --- a/pkg/code/server/grpc/transaction/v2/onramp_test.go +++ b/pkg/code/server/grpc/transaction/v2/onramp_test.go @@ -1,5 +1,6 @@ package transaction_v2 +/* import ( "testing" @@ -51,3 +52,4 @@ func TestDeclareFiatOnrampPurchaseAttempt_InvalidPurchaseAmount(t *testing.T) { server.assertFiatOnrampPurchasedDetailsNotSaved(t, nonce) } +*/ diff --git a/pkg/code/server/grpc/transaction/v2/proof.go b/pkg/code/server/grpc/transaction/v2/proof.go index 5336b781..cee818df 100644 --- a/pkg/code/server/grpc/transaction/v2/proof.go +++ b/pkg/code/server/grpc/transaction/v2/proof.go @@ -1,22 +1,6 @@ package transaction_v2 -import ( - "context" - "encoding/hex" - "errors" - "sync" - "time" - - "github.com/mr-tron/base58/base58" - - commitment_worker "github.com/code-payments/code-server/pkg/code/async/commitment" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/merkletree" -) - +/* type refreshingMerkleTree struct { tree *merkletree.MerkleTree lastRefreshedAt time.Time @@ -269,3 +253,4 @@ func getCachedMerkleTreeForTreasury(ctx context.Context, data code_data.Provider return cached.tree, nil } +*/ diff --git a/pkg/code/server/grpc/transaction/v2/swap.go b/pkg/code/server/grpc/transaction/v2/swap.go index 137328df..ba3ae312 100644 --- a/pkg/code/server/grpc/transaction/v2/swap.go +++ b/pkg/code/server/grpc/transaction/v2/swap.go @@ -359,11 +359,11 @@ func (s *transactionServer) Swap(streamer transactionpb.Transaction_SwapServer) return handleSwapStructuredError( streamer, transactionpb.SwapResponse_Error_SIGNATURE_ERROR, - toInvalidSignatureErrorDetails(0, txn, submitSignatureReq.Signature), + toInvalidTxnSignatureErrorDetails(0, txn, submitSignatureReq.Signature), ) } - copy(txn.Signatures[clientSignatureIndex][:], submitSignatureReq.Signature.Value) + copy(txn.Signatures[1][:], submitSignatureReq.Signature.Value) txn.Sign(s.swapSubsidizer.PrivateKey().ToBytes()) log = log.WithField("transaction_id", base58.Encode(txn.Signature())) @@ -520,8 +520,8 @@ func (s *transactionServer) bestEffortNotifyUserOfSwapInProgress(ctx context.Con } switch typed := protoChatMessage.Content[0].Type.(type) { - case *chatpb.Content_Localized: - if typed.Localized.KeyOrText != localization.ChatMessageUsdcDeposited { + case *chatpb.Content_ServerLocalized: + if typed.ServerLocalized.KeyOrText != localization.ChatMessageUsdcDeposited { return nil } } diff --git a/pkg/code/server/grpc/transaction/v2/testutil.go b/pkg/code/server/grpc/transaction/v2/testutil.go index 3dbb5e88..9612d1a1 100644 --- a/pkg/code/server/grpc/transaction/v2/testutil.go +++ b/pkg/code/server/grpc/transaction/v2/testutil.go @@ -1,5 +1,6 @@ package transaction_v2 +/* import ( "bytes" "context" @@ -6180,3 +6181,4 @@ func getProtoChatMessage(t *testing.T, record *chat.Message) *chatpb.ChatMessage require.NoError(t, proto.Unmarshal(record.Data, &protoMessage)) return &protoMessage } +*/ diff --git a/pkg/code/transaction/virtual_instruction.go b/pkg/code/transaction/virtual_instruction.go new file mode 100644 index 00000000..0cf2ebe4 --- /dev/null +++ b/pkg/code/transaction/virtual_instruction.go @@ -0,0 +1,94 @@ +package transaction + +import ( + "crypto/sha256" + + "github.com/code-payments/code-server/pkg/code/common" + "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +func GetVirtualTransferWithAuthorityHash( + nonce *common.Account, + bh solana.Blockhash, + + source *common.TimelockAccounts, + destination *common.Account, + kinAmountInQuarks uint64, +) (*cvm.Hash, error) { + memoInstruction, err := MakeKreMemoInstruction() + if err != nil { + return nil, err + } + + transferWithAuthorityInstruction, err := source.GetTransferWithAuthorityInstruction(destination, kinAmountInQuarks) + if err != nil { + return nil, err + } + + instructions := []solana.Instruction{ + memoInstruction, + transferWithAuthorityInstruction, + } + txn, err := MakeNoncedTransaction(nonce, bh, instructions...) + if err != nil { + return nil, err + } + + hash := getVirtualTransactionHash(&txn) + return &hash, nil +} + +func GetVirtualCloseAccountWithBalanceHash( + nonce *common.Account, + bh solana.Blockhash, + + source *common.TimelockAccounts, + destination *common.Account, +) (*cvm.Hash, error) { + memoInstruction, err := MakeKreMemoInstruction() + if err != nil { + return nil, err + } + + revokeLockInstruction, err := source.GetRevokeLockWithAuthorityInstruction() + if err != nil { + return nil, err + } + + deactivateLockInstruction, err := source.GetDeactivateInstruction() + if err != nil { + return nil, err + } + + withdrawInstruction, err := source.GetWithdrawInstruction(destination) + if err != nil { + return nil, err + } + + closeInstruction, err := source.GetCloseAccountsInstruction() + if err != nil { + return nil, err + } + + instructions := []solana.Instruction{ + memoInstruction, + revokeLockInstruction, + deactivateLockInstruction, + withdrawInstruction, + closeInstruction, + } + txn, err := MakeNoncedTransaction(nonce, bh, instructions...) + if err != nil { + return nil, err + } + + hash := getVirtualTransactionHash(&txn) + return &hash, nil +} + +func getVirtualTransactionHash(txn *solana.Transaction) cvm.Hash { + hasher := sha256.New() + hasher.Write(txn.Marshal()) + return cvm.Hash(hasher.Sum(nil)) +} From 52e9b63a1dda699f22dc506e96ce627373ee32cd Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 22 Aug 2024 16:04:44 -0400 Subject: [PATCH 30/79] Make server VM branch buildable (#172) --- pkg/code/async/geyser/backup.go | 78 +-- pkg/code/async/geyser/external_deposit.go | 451 +++++++++--------- pkg/code/async/geyser/handler.go | 77 +-- pkg/code/async/geyser/timelock.go | 100 +--- pkg/code/async/treasury/testutil.go | 3 +- pkg/code/chat/message_code_team.go | 4 +- pkg/code/chat/message_kin_purchases.go | 12 +- pkg/code/chat/sender_test.go | 4 +- .../data/commitment/postgres/store_test.go | 2 +- .../anti_money_laundering_test.go | 5 +- pkg/code/push/notifications.go | 12 +- pkg/code/server/grpc/account/server.go | 20 - pkg/code/server/grpc/account/server_test.go | 315 ++---------- pkg/code/server/grpc/chat/server.go | 10 +- pkg/code/server/grpc/chat/server_test.go | 22 +- pkg/code/server/grpc/messaging/server.go | 3 +- pkg/code/server/grpc/messaging/testutil.go | 4 +- pkg/code/server/grpc/phone/server_test.go | 4 +- .../server/grpc/transaction/v2/airdrop.go | 33 +- pkg/code/server/grpc/user/server_test.go | 4 +- 20 files changed, 332 insertions(+), 831 deletions(-) diff --git a/pkg/code/async/geyser/backup.go b/pkg/code/async/geyser/backup.go index 96d47a7c..2bdc9f4e 100644 --- a/pkg/code/async/geyser/backup.go +++ b/pkg/code/async/geyser/backup.go @@ -2,15 +2,12 @@ package async_geyser import ( "context" - "sync" "time" "github.com/newrelic/go-agent/v3/newrelic" "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/metrics" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) // Backup system workers can be found here. This is necessary because we can't rely @@ -44,43 +41,11 @@ func (p *service) backupTimelockStateWorker(serviceCtx context.Context, interval nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) m := nr.StartTransaction("async__geyser_consumer_service__backup_timelock_state_worker") defer m.End() - tracedCtx := newrelic.NewContext(serviceCtx, m) + //tracedCtx := newrelic.NewContext(serviceCtx, m) - jobSucceeded := true - - // Find and process unlocked timelock accounts unlocking between [+21 - n days, +21 days], - // which enables a retry mechanism across time. - for i := uint8(0); i <= uint8(p.conf.backupTimelockWorkerDaysChecked.Get(tracedCtx)); i++ { - daysUntilUnlock := timelock_token_v1.DefaultNumDaysLocked - i - - addresses, slot, err := findUnlockedTimelockV1Accounts(tracedCtx, p.data, daysUntilUnlock) - if err != nil { - m.NoticeError(err) - log.WithError(err).Warn("failure getting unlocked timelock accounts") - jobSucceeded = false - continue - } - - log.Infof("found %d timelock accounts unlocking in %d days", len(addresses), daysUntilUnlock) - - for _, address := range addresses { - log := log.WithField("account", address) - - stateAccount, err := common.NewAccountFromPublicKeyString(address) - if err != nil { - log.WithError(err).Warn("invalid state account address") - continue - } - - err = updateTimelockV1AccountCachedState(tracedCtx, p.data, stateAccount, slot) - if err != nil { - m.NoticeError(err) - log.WithError(err).Warn("failure updating cached timelock account state") - jobSucceeded = false - continue - } - } - } + jobSucceeded := false + + // todo: implement me p.metricStatusLock.Lock() p.unlockedTimelockAccountsSynced = jobSucceeded @@ -116,40 +81,9 @@ func (p *service) backupExternalDepositWorker(serviceCtx context.Context, interv nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) m := nr.StartTransaction("async__geyser_consumer_service__backup_external_deposit_worker") defer m.End() - tracedCtx := newrelic.NewContext(serviceCtx, m) - - accountInfoRecords, err := p.data.GetPrioritizedAccountInfosRequiringDepositSync(tracedCtx, p.conf.backupExternalDepositWorkerCount.Get(tracedCtx)) - if err != nil { - if err != account.ErrAccountInfoNotFound { - m.NoticeError(err) - log.WithError(err).Warn("failure getting accounts to sync external deposits") - } - return - } - - var wg sync.WaitGroup - for _, accountInfoRecord := range accountInfoRecords { - vault, err := common.NewAccountFromPublicKeyString(accountInfoRecord.TokenAccount) - if err != nil { - log.WithError(err).WithField("account", accountInfoRecord.TokenAccount).Warn("invalid token account") - continue - } - - wg.Add(1) + // tracedCtx := newrelic.NewContext(serviceCtx, m) - go func(vault *common.Account) { - defer wg.Done() - - log := log.WithField("account", vault.PublicKey().ToBase58()) - - err := fixMissingExternalDeposits(tracedCtx, p.conf, p.data, p.pusher, vault) - if err != nil { - m.NoticeError(err) - log.WithError(err).Warn("failure fixing missing external deposits") - } - }(vault) - } - wg.Wait() + // todo: implement me }() case <-serviceCtx.Done(): return serviceCtx.Err() diff --git a/pkg/code/async/geyser/external_deposit.go b/pkg/code/async/geyser/external_deposit.go index 1b5939ac..969392ff 100644 --- a/pkg/code/async/geyser/external_deposit.go +++ b/pkg/code/async/geyser/external_deposit.go @@ -21,23 +21,19 @@ import ( "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/balance" "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/deposit" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/onramp" - "github.com/code-payments/code-server/pkg/code/data/transaction" "github.com/code-payments/code-server/pkg/code/push" "github.com/code-payments/code-server/pkg/code/thirdparty" currency_lib "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/kin" push_lib "github.com/code-payments/code-server/pkg/push" - "github.com/code-payments/code-server/pkg/retry" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/usdc" ) +// todo: needs to be reimagined for the VM + const ( codeMemoValue = "ZTAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" ) @@ -66,14 +62,8 @@ func fixMissingExternalDeposits(ctx context.Context, conf *conf, data code_data. return markDepositsAsSynced(ctx, data, vault) } -// Note: This puts an upper bound to how far back in history we'll search -// -// todo: We can track the furthest succesful signature, so we have a bound. -// This would also enable us to not reprocess transactions. We'll need to be -// careful to check commitment status, since GetBlockchainHistory could return -// non-finalized transactions. func findPotentialExternalDeposits(ctx context.Context, data code_data.Provider, vault *common.Account) ([]string, error) { - var res []string + /*var res []string var cursor []byte var totalTransactionsFound int for { @@ -120,268 +110,273 @@ func findPotentialExternalDeposits(ctx context.Context, data code_data.Provider, cursor = query.Cursor(history[len(history)-1].Signature[:]) } + */ + return nil, errors.New("not implemented") } func processPotentialExternalDeposit(ctx context.Context, conf *conf, data code_data.Provider, pusher push_lib.Provider, signature string, tokenAccount *common.Account) error { - // Avoid reprocessing deposits we've recently seen and processed. Particularly, - // the backup process will likely be triggered in frequent bursts, so this is - // just an optimization around that. - cacheKey := getSyncedDepositCacheKey(signature, tokenAccount) - _, ok := syncedDepositCache.Retrieve(cacheKey) - if ok { - return nil - } - - decodedSignature, err := base58.Decode(signature) - if err != nil { - return errors.Wrap(err, "invalid signature") - } - var typedSignature solana.Signature - copy(typedSignature[:], decodedSignature) - - // Is this transaction a fulfillment? If so, it cannot be an external deposit. - _, err = data.GetFulfillmentBySignature(ctx, signature) - if err == nil { - return nil - } else if err != fulfillment.ErrFulfillmentNotFound { - return errors.Wrap(err, "error getting fulfillment record") - } - - // Grab transaction token balances to get net quark balances from this transaction. - // This enables us to avoid parsing transaction data and generically handle any - // kind of transaction. It's far too complicated if we need to inspect individual - // instructions. - var tokenBalances *solana.TransactionTokenBalances - _, err = retry.Retry( - func() error { - tokenBalances, err = data.GetBlockchainTransactionTokenBalances(ctx, signature) - return err - }, - waitForFinalizationRetryStrategies..., - ) - if err != nil { - return errors.Wrap(err, "error getting transaction token balances") - } - - // Check whether the Code subsidizer was involved in this transaction. If it is, then - // it cannot be an external deposit. - for _, account := range tokenBalances.Accounts { - if account == common.GetSubsidizer().PublicKey().ToBase58() { + /* + // Avoid reprocessing deposits we've recently seen and processed. Particularly, + // the backup process will likely be triggered in frequent bursts, so this is + // just an optimization around that. + cacheKey := getSyncedDepositCacheKey(signature, tokenAccount) + _, ok := syncedDepositCache.Retrieve(cacheKey) + if ok { return nil } - } - - deltaQuarks, err := getDeltaQuarksFromTokenBalances(tokenAccount, tokenBalances) - if err != nil { - return errors.Wrap(err, "error getting delta quarks from token balances") - } - - // Transaction did not positively affect token account balance, so no new funds - // were externally deposited into the account. - if deltaQuarks <= 0 { - return nil - } - accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, tokenAccount.PublicKey().ToBase58()) - if err != nil { - return errors.Wrap(err, "error getting account info record") - } + decodedSignature, err := base58.Decode(signature) + if err != nil { + return errors.Wrap(err, "invalid signature") + } + var typedSignature solana.Signature + copy(typedSignature[:], decodedSignature) - chatMessageReceiver, err := common.NewAccountFromPublicKeyString(accountInfoRecord.OwnerAccount) - if err != nil { - return errors.Wrap(err, "invalid owner account") - } + // Is this transaction a fulfillment? If so, it cannot be an external deposit. + _, err = data.GetFulfillmentBySignature(ctx, signature) + if err == nil { + return nil + } else if err != fulfillment.ErrFulfillmentNotFound { + return errors.Wrap(err, "error getting fulfillment record") + } + + // Grab transaction token balances to get net quark balances from this transaction. + // This enables us to avoid parsing transaction data and generically handle any + // kind of transaction. It's far too complicated if we need to inspect individual + // instructions. + var tokenBalances *solana.TransactionTokenBalances + _, err = retry.Retry( + func() error { + tokenBalances, err = data.GetBlockchainTransactionTokenBalances(ctx, signature) + return err + }, + waitForFinalizationRetryStrategies..., + ) + if err != nil { + return errors.Wrap(err, "error getting transaction token balances") + } - blockTime := time.Now() - if tokenBalances.BlockTime != nil { - blockTime = *tokenBalances.BlockTime - } + // Check whether the Code subsidizer was involved in this transaction. If it is, then + // it cannot be an external deposit. + for _, account := range tokenBalances.Accounts { + if account == common.GetSubsidizer().PublicKey().ToBase58() { + return nil + } + } - // Use the account type to determine how we'll process this external deposit - // - // todo: Below logic is beginning to get messy and might be in need of a - // refactor soon - switch accountInfoRecord.AccountType { + deltaQuarks, err := getDeltaQuarksFromTokenBalances(tokenAccount, tokenBalances) + if err != nil { + return errors.Wrap(err, "error getting delta quarks from token balances") + } - case commonpb.AccountType_PRIMARY, commonpb.AccountType_RELATIONSHIP: - // Check whether we've previously processed this external deposit - _, err = data.GetExternalDeposit(ctx, signature, tokenAccount.PublicKey().ToBase58()) - if err == nil { - syncedDepositCache.Insert(cacheKey, true, 1) + // Transaction did not positively affect token account balance, so no new funds + // were externally deposited into the account. + if deltaQuarks <= 0 { return nil } - isCodeSwap, usdcSwapAccount, usdcQuarksSwapped, err := getCodeSwapMetadata(ctx, conf, tokenBalances) + accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, tokenAccount.PublicKey().ToBase58()) if err != nil { - return errors.Wrap(err, "error getting code swap metadata") + return errors.Wrap(err, "error getting account info record") } - var usdMarketValue float64 - if isCodeSwap { - usdMarketValue = float64(usdcQuarksSwapped) / float64(usdc.QuarksPerUsdc) - } else { - usdExchangeRecord, err := data.GetExchangeRate(ctx, currency_lib.USD, time.Now()) - if err != nil { - return errors.Wrap(err, "error getting usd rate") - } - usdMarketValue = usdExchangeRecord.Rate * float64(deltaQuarks) / float64(kin.QuarksPerKin) + chatMessageReceiver, err := common.NewAccountFromPublicKeyString(accountInfoRecord.OwnerAccount) + if err != nil { + return errors.Wrap(err, "invalid owner account") } - if isCodeSwap { - // Checkpoint the Code swap account balance, to minimize chances a - // stale RPC node results in a double counting of funds - bestEffortCacheExternalAccountBalance(ctx, data, usdcSwapAccount, tokenBalances) + blockTime := time.Now() + if tokenBalances.BlockTime != nil { + blockTime = *tokenBalances.BlockTime } - // For a consistent payment history list + // Use the account type to determine how we'll process this external deposit // - // Deprecated in favour of chats (for history purposes) - intentRecord := &intent.Record{ - IntentId: fmt.Sprintf("%s-%s", signature, tokenAccount.PublicKey().ToBase58()), - IntentType: intent.ExternalDeposit, - - InitiatorOwnerAccount: tokenBalances.Accounts[0], // The fee payer - - ExternalDepositMetadata: &intent.ExternalDepositMetadata{ - DestinationOwnerAccount: accountInfoRecord.OwnerAccount, - DestinationTokenAccount: tokenAccount.PublicKey().ToBase58(), - Quantity: uint64(deltaQuarks), - UsdMarketValue: usdMarketValue, - }, + // todo: Below logic is beginning to get messy and might be in need of a + // refactor soon + switch accountInfoRecord.AccountType { - State: intent.StateConfirmed, - CreatedAt: time.Now(), - } - err = data.SaveIntent(ctx, intentRecord) - if err != nil { - return errors.Wrap(err, "error saving intent record") - } + case commonpb.AccountType_PRIMARY, commonpb.AccountType_RELATIONSHIP: + // Check whether we've previously processed this external deposit + _, err = data.GetExternalDeposit(ctx, signature, tokenAccount.PublicKey().ToBase58()) + if err == nil { + syncedDepositCache.Insert(cacheKey, true, 1) + return nil + } - if isCodeSwap { - purchases, err := getPurchasesFromSwap( - ctx, - conf, - data, - signature, - usdcSwapAccount, - usdcQuarksSwapped, - ) + isCodeSwap, usdcSwapAccount, usdcQuarksSwapped, err := getCodeSwapMetadata(ctx, conf, tokenBalances) if err != nil { - return errors.Wrap(err, "error getting swap purchases") + return errors.Wrap(err, "error getting code swap metadata") } - var protoPurchases []*transactionpb.ExchangeDataWithoutRate - for _, purchase := range purchases { - protoPurchases = append(protoPurchases, purchase.protoExchangeData) - recordBuyModulePurchaseCompletedEvent( - ctx, - purchase.deviceType, - purchase.purchaseInitiationTime, - purchase.usdcDepositTime, - ) + var usdMarketValue float64 + if isCodeSwap { + usdMarketValue = float64(usdcQuarksSwapped) / float64(usdc.QuarksPerUsdc) + } else { + usdExchangeRecord, err := data.GetExchangeRate(ctx, currency_lib.USD, time.Now()) + if err != nil { + return errors.Wrap(err, "error getting usd rate") + } + usdMarketValue = usdExchangeRecord.Rate * float64(deltaQuarks) / float64(kin.QuarksPerKin) + } + + if isCodeSwap { + // Checkpoint the Code swap account balance, to minimize chances a + // stale RPC node results in a double counting of funds + bestEffortCacheExternalAccountBalance(ctx, data, usdcSwapAccount, tokenBalances) } - chatMessage, err := chat_util.ToKinAvailableForUseMessage(signature, blockTime, protoPurchases...) + // For a consistent payment history list + // + // Deprecated in favour of chats (for history purposes) + intentRecord := &intent.Record{ + IntentId: fmt.Sprintf("%s-%s", signature, tokenAccount.PublicKey().ToBase58()), + IntentType: intent.ExternalDeposit, + + InitiatorOwnerAccount: tokenBalances.Accounts[0], // The fee payer + + ExternalDepositMetadata: &intent.ExternalDepositMetadata{ + DestinationOwnerAccount: accountInfoRecord.OwnerAccount, + DestinationTokenAccount: tokenAccount.PublicKey().ToBase58(), + Quantity: uint64(deltaQuarks), + UsdMarketValue: usdMarketValue, + }, + + State: intent.StateConfirmed, + CreatedAt: time.Now(), + } + err = data.SaveIntent(ctx, intentRecord) if err != nil { - return errors.Wrap(err, "error creating chat message") + return errors.Wrap(err, "error saving intent record") } - canPush, err := chat_util.SendKinPurchasesMessage(ctx, data, chatMessageReceiver, chatMessage) - switch err { - case nil: - if canPush { - push.SendChatMessagePushNotification( + if isCodeSwap { + purchases, err := getPurchasesFromSwap( + ctx, + conf, + data, + signature, + usdcSwapAccount, + usdcQuarksSwapped, + ) + if err != nil { + return errors.Wrap(err, "error getting swap purchases") + } + + var protoPurchases []*transactionpb.ExchangeDataWithoutRate + for _, purchase := range purchases { + protoPurchases = append(protoPurchases, purchase.protoExchangeData) + recordBuyModulePurchaseCompletedEvent( ctx, - data, - pusher, - chat_util.KinPurchasesName, - chatMessageReceiver, - chatMessage, + purchase.deviceType, + purchase.purchaseInitiationTime, + purchase.usdcDepositTime, ) } - case chat.ErrMessageAlreadyExists: - default: - return errors.Wrap(err, "error sending chat message") + + chatMessage, err := chat_util.ToKinAvailableForUseMessage(signature, blockTime, protoPurchases...) + if err != nil { + return errors.Wrap(err, "error creating chat message") + } + + canPush, err := chat_util.SendKinPurchasesMessage(ctx, data, chatMessageReceiver, chatMessage) + switch err { + case nil: + if canPush { + push.SendChatMessagePushNotification( + ctx, + data, + pusher, + chat_util.KinPurchasesName, + chatMessageReceiver, + chatMessage, + ) + } + case chat.ErrMessageAlreadyExists: + default: + return errors.Wrap(err, "error sending chat message") + } + } else { + err = chat_util.SendCashTransactionsExchangeMessage(ctx, data, intentRecord) + if err != nil { + return errors.Wrap(err, "error updating cash transactions chat") + } + _, err = chat_util.SendMerchantExchangeMessage(ctx, data, intentRecord, nil) + if err != nil { + return errors.Wrap(err, "error updating merchant chat") + } + + push.SendDepositPushNotification(ctx, data, pusher, tokenAccount, uint64(deltaQuarks)) } - } else { - err = chat_util.SendCashTransactionsExchangeMessage(ctx, data, intentRecord) - if err != nil { - return errors.Wrap(err, "error updating cash transactions chat") + + // For tracking in balances + externalDepositRecord := &deposit.Record{ + Signature: signature, + Destination: tokenAccount.PublicKey().ToBase58(), + Amount: uint64(deltaQuarks), + UsdMarketValue: usdMarketValue, + + Slot: tokenBalances.Slot, + ConfirmationState: transaction.ConfirmationFinalized, + + CreatedAt: time.Now(), } - _, err = chat_util.SendMerchantExchangeMessage(ctx, data, intentRecord, nil) + err = data.SaveExternalDeposit(ctx, externalDepositRecord) if err != nil { - return errors.Wrap(err, "error updating merchant chat") + return errors.Wrap(err, "error creating external deposit record") } - push.SendDepositPushNotification(ctx, data, pusher, tokenAccount, uint64(deltaQuarks)) - } - - // For tracking in balances - externalDepositRecord := &deposit.Record{ - Signature: signature, - Destination: tokenAccount.PublicKey().ToBase58(), - Amount: uint64(deltaQuarks), - UsdMarketValue: usdMarketValue, + syncedDepositCache.Insert(cacheKey, true, 1) - Slot: tokenBalances.Slot, - ConfirmationState: transaction.ConfirmationFinalized, + return nil - CreatedAt: time.Now(), - } - err = data.SaveExternalDeposit(ctx, externalDepositRecord) - if err != nil { - return errors.Wrap(err, "error creating external deposit record") - } + case commonpb.AccountType_SWAP: + bestEffortCacheExternalAccountBalance(ctx, data, tokenAccount, tokenBalances) + + // go delayedUsdcDepositProcessing( + // ctx, + // conf, + // data, + // pusher, + // chatMessageReceiver, + // tokenAccount, + // signature, + // blockTime, + // ) + + owner, err := common.NewAccountFromPublicKeyString(accountInfoRecord.OwnerAccount) + if err != nil { + return errors.Wrap(err, "invalid owner account") + } - syncedDepositCache.Insert(cacheKey, true, 1) + // Best-effort attempt to get the client to trigger a Swap RPC call now + go push.SendTriggerSwapRpcPushNotification( + ctx, + data, + pusher, + owner, + ) - return nil + // Have the account pulled by the swap retry worker + err = markRequiringSwapRetries(ctx, data, accountInfoRecord) + if err != nil { + return err + } - case commonpb.AccountType_SWAP: - bestEffortCacheExternalAccountBalance(ctx, data, tokenAccount, tokenBalances) - - // go delayedUsdcDepositProcessing( - // ctx, - // conf, - // data, - // pusher, - // chatMessageReceiver, - // tokenAccount, - // signature, - // blockTime, - // ) - - owner, err := common.NewAccountFromPublicKeyString(accountInfoRecord.OwnerAccount) - if err != nil { - return errors.Wrap(err, "invalid owner account") - } + syncedDepositCache.Insert(cacheKey, true, 1) - // Best-effort attempt to get the client to trigger a Swap RPC call now - go push.SendTriggerSwapRpcPushNotification( - ctx, - data, - pusher, - owner, - ) + return nil - // Have the account pulled by the swap retry worker - err = markRequiringSwapRetries(ctx, data, accountInfoRecord) - if err != nil { - return err + default: + // Mark anything other than deposit or swap accounts as synced and move on without + // saving anything. There's a potential someone could overutilize our treasury + // by depositing large sums into temporary or bucket accounts, which have + // more lenient checks ATM. We'll deal with these adhoc as they arise. + syncedDepositCache.Insert(cacheKey, true, 1) + return nil } - - syncedDepositCache.Insert(cacheKey, true, 1) - - return nil - - default: - // Mark anything other than deposit or swap accounts as synced and move on without - // saving anything. There's a potential someone could overutilize our treasury - // by depositing large sums into temporary or bucket accounts, which have - // more lenient checks ATM. We'll deal with these adhoc as they arise. - syncedDepositCache.Insert(cacheKey, true, 1) - return nil - } + */ + return errors.New("not implemented") } func markDepositsAsSynced(ctx context.Context, data code_data.Provider, vault *common.Account) error { diff --git a/pkg/code/async/geyser/handler.go b/pkg/code/async/geyser/handler.go index f44e8f68..2f00b35f 100644 --- a/pkg/code/async/geyser/handler.go +++ b/pkg/code/async/geyser/handler.go @@ -11,10 +11,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/kin" push_lib "github.com/code-payments/code-server/pkg/push" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/solana/token" ) @@ -147,80 +144,8 @@ func (h *TokenProgramAccountHandler) Handle(ctx context.Context, update *geyserp return processPotentialExternalDeposit(ctx, h.conf, h.data, h.pusher, *update.TxSignature, tokenAccount) } -type TimelockV1ProgramAccountHandler struct { - data code_data.Provider -} - -func NewTimelockV1ProgramAccountHandler(data code_data.Provider) ProgramAccountUpdateHandler { - return &TimelockV1ProgramAccountHandler{ - data: data, - } -} - -func (h *TimelockV1ProgramAccountHandler) Handle(ctx context.Context, update *geyserpb.AccountUpdate) error { - if !bytes.Equal(update.Owner, timelock_token_v1.PROGRAM_ID) { - return ErrUnexpectedProgramOwner - } - - if len(update.Data) > 0 { - var unmarshalled timelock_token_v1.TimelockAccount - err := unmarshalled.Unmarshal(update.Data) - if err != nil { - return errors.Wrap(err, "error unmarshalling account data from update") - } - - // Not a Kin account, so filter it out - if !bytes.Equal(unmarshalled.Mint, kin.TokenMint) { - return nil - } - - // Not managed by Code, so filter it out - if !bytes.Equal(unmarshalled.TimeAuthority, common.GetSubsidizer().PublicKey().ToBytes()) { - return nil - } - - // Account is locked, so filter it out. Scheduler success handlers ensure - // we properly update state from unknown to locked state. We don't care if - // the account remains locked. We're really just interested in external - // unlocks. Skip the update to reduce load. - if unmarshalled.VaultState == timelock_token_v1.StateLocked { - return nil - } - } - - stateAccount, err := common.NewAccountFromPublicKeyBytes(update.Pubkey) - if err != nil { - return errors.Wrap(err, "invalid state account") - } - - // Go out to the blockchain to fetch finalized account state. Don't trust the update. - return updateTimelockV1AccountCachedState(ctx, h.data, stateAccount, update.Slot) -} - -type SplitterProgramAccountHandler struct { - data code_data.Provider -} - -func NewSplitterProgramAccountHandler(data code_data.Provider) ProgramAccountUpdateHandler { - return &SplitterProgramAccountHandler{ - data: data, - } -} - -func (h *SplitterProgramAccountHandler) Handle(ctx context.Context, update *geyserpb.AccountUpdate) error { - if !bytes.Equal(update.Owner, splitter_token.PROGRAM_ID) { - return ErrUnexpectedProgramOwner - } - - // Nothing to do, we don't care about updates here yet. Everything is handled - // externally atm in the fulfillment and treasury workers. - return nil -} - func initializeProgramAccountUpdateHandlers(conf *conf, data code_data.Provider, pusher push_lib.Provider) map[string]ProgramAccountUpdateHandler { return map[string]ProgramAccountUpdateHandler{ - base58.Encode(token.ProgramKey): NewTokenProgramAccountHandler(conf, data, pusher), - base58.Encode(timelock_token_v1.PROGRAM_ID): NewTimelockV1ProgramAccountHandler(data), - base58.Encode(splitter_token.PROGRAM_ID): NewSplitterProgramAccountHandler(data), + base58.Encode(token.ProgramKey): NewTokenProgramAccountHandler(conf, data, pusher), } } diff --git a/pkg/code/async/geyser/timelock.go b/pkg/code/async/geyser/timelock.go index 7249b1b3..45c529d9 100644 --- a/pkg/code/async/geyser/timelock.go +++ b/pkg/code/async/geyser/timelock.go @@ -2,110 +2,14 @@ package async_geyser import ( "context" - "encoding/binary" - "time" - "github.com/mr-tron/base58" "github.com/pkg/errors" - "github.com/code-payments/code-server/pkg/retry" - "github.com/code-payments/code-server/pkg/retry/backoff" - "github.com/code-payments/code-server/pkg/solana" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" - "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/timelock" ) -const ( - secondsPerDay = 86400 // 60sec * 60min * 24hrs = 86400 - timelockV1UnlockTimeOffset = 172 -) +// todo: needs to be reimagined for the VM func findUnlockedTimelockV1Accounts(ctx context.Context, data code_data.Provider, daysFromToday uint8) ([]string, uint64, error) { - ts := time.Now().Unix() + int64(daysFromToday)*secondsPerDay - - // Normalize the unlock time to the start of the UTC day. Source reference: - // https://github.com/code-payments/code-program-library/blob/901296f86ee9202408001cb57abc5218cecf2457/timelock-token/programs/timelock-token/src/lib.rs#L86-L93 - tsAtStartofUtc := ts - if ts%secondsPerDay != 0 { - tsAtStartofUtc = ts + (secondsPerDay - (ts % secondsPerDay)) - } - - dataFilterValue := make([]byte, 8) - binary.LittleEndian.PutUint64(dataFilterValue, uint64(tsAtStartofUtc)) - - var addresses []string - var slot uint64 - var err error - _, err = retry.Retry( - func() error { - addresses, slot, err = data.GetBlockchainFilteredProgramAccounts( - ctx, - base58.Encode(timelock_token_v1.PROGRAM_ID), - timelockV1UnlockTimeOffset, - dataFilterValue, - ) - return err - }, - retry.NonRetriableErrors(context.Canceled), - retry.Limit(3), - retry.Backoff(backoff.BinaryExponential(time.Second), 5*time.Second), - ) - if err != nil { - return nil, 0, errors.Wrap(err, "error getting filtered timelock program accounts") - } - return addresses, slot, nil -} - -func updateTimelockV1AccountCachedState(ctx context.Context, data code_data.Provider, stateAccount *common.Account, minSlot uint64) error { - timelockRecord, err := data.GetTimelockByAddress(ctx, stateAccount.PublicKey().ToBase58()) - if err == timelock.ErrTimelockNotFound { - // Not a timelock we care about - return nil - } else if err != nil { - return errors.Wrap(err, "error getting timelock record") - } - - var finalizedData []byte - var finalizedSlot uint64 - _, err = retry.Retry( - func() error { - finalizedData, finalizedSlot, err = data.GetBlockchainAccountDataAfterBlock(ctx, stateAccount.PublicKey().ToBase58(), minSlot) - return err - }, - append( - []retry.Strategy{ - retry.NonRetriableErrors(solana.ErrNoAccountInfo), - }, - waitForFinalizationRetryStrategies..., - )..., - ) - if err != nil && err != solana.ErrNoAccountInfo { - return errors.Wrap(err, "error getting finalized account data") - } - - if err == solana.ErrNoAccountInfo { - timelockRecord.VaultState = timelock_token_v1.StateClosed - timelockRecord.Block = finalizedSlot - } else { - var finalizedState timelock_token_v1.TimelockAccount - err = finalizedState.Unmarshal(finalizedData) - if err != nil { - return errors.Wrap(err, "error unmarshalling account data from finalized data") - } - - err = timelockRecord.UpdateFromV1ProgramAccount(&finalizedState, finalizedSlot) - if err == timelock.ErrStaleTimelockState { - return nil - } else if err != nil { - return errors.Wrap(err, "error updating timelock record locally") - } - } - - err = data.SaveTimelock(ctx, timelockRecord) - if err != nil && err != timelock.ErrStaleTimelockState { - return errors.Wrap(err, "error saving timelock record") - } - return nil + return nil, 0, errors.New("not implemented") } diff --git a/pkg/code/async/treasury/testutil.go b/pkg/code/async/treasury/testutil.go index 55a826f0..3e292d75 100644 --- a/pkg/code/async/treasury/testutil.go +++ b/pkg/code/async/treasury/testutil.go @@ -177,7 +177,8 @@ func (e *testEnv) simulateCommitments(t *testing.T, count int, recentRoot string var commitmentRecords []*commitment.Record for i := 0; i < count; i++ { commitmentRecord := &commitment.Record{ - Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), + VaultAddress: testutil.NewRandomAccount(t).PublicKey().ToBase58(), Pool: e.treasuryPool.Address, RecentRoot: recentRoot, diff --git a/pkg/code/chat/message_code_team.go b/pkg/code/chat/message_code_team.go index fe8a7049..536370a3 100644 --- a/pkg/code/chat/message_code_team.go +++ b/pkg/code/chat/message_code_team.go @@ -48,8 +48,8 @@ func newIncentiveMessage(localizedTextKey string, intentRecord *intent.Record) ( content := []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localizedTextKey, }, }, diff --git a/pkg/code/chat/message_kin_purchases.go b/pkg/code/chat/message_kin_purchases.go index 1ec10b68..f9a2c6fd 100644 --- a/pkg/code/chat/message_kin_purchases.go +++ b/pkg/code/chat/message_kin_purchases.go @@ -40,8 +40,8 @@ func SendKinPurchasesMessage(ctx context.Context, data code_data.Provider, recei func ToUsdcDepositedMessage(signature string, ts time.Time) (*chatpb.ChatMessage, error) { content := []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localization.ChatMessageUsdcDeposited, }, }, @@ -60,8 +60,8 @@ func NewUsdcBeingConvertedMessage(ts time.Time) (*chatpb.ChatMessage, error) { content := []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localization.ChatMessageUsdcBeingConverted, }, }, @@ -79,8 +79,8 @@ func ToKinAvailableForUseMessage(signature string, ts time.Time, purchases ...*t content := []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localization.ChatMessageKinAvailableForUse, }, }, diff --git a/pkg/code/chat/sender_test.go b/pkg/code/chat/sender_test.go index 7625a1f3..3438b3c8 100644 --- a/pkg/code/chat/sender_test.go +++ b/pkg/code/chat/sender_test.go @@ -159,8 +159,8 @@ func newRandomChatMessage(t *testing.T, contentLength int) *chatpb.ChatMessage { var content []*chatpb.Content for i := 0; i < contentLength; i++ { content = append(content, &chatpb.Content{ - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: fmt.Sprintf("key%d", rand.Uint32()), }, }, diff --git a/pkg/code/data/commitment/postgres/store_test.go b/pkg/code/data/commitment/postgres/store_test.go index 9f8fb196..de8fdb5c 100644 --- a/pkg/code/data/commitment/postgres/store_test.go +++ b/pkg/code/data/commitment/postgres/store_test.go @@ -45,7 +45,7 @@ const ( created_at TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT codewallet__core_commitment__uniq__address UNIQUE (address), - CONSTRAINT codewallet__core_commitment__uniq__vault_address UNIQUE (vault_address), + CONSTRAINT codewallet__core_commitment__uniq__vault UNIQUE (vault), CONSTRAINT codewallet__core_commitment__uniq__transcript UNIQUE (transcript), CONSTRAINT codewallet__core_commitment__uniq__intent__and__action_id UNIQUE (intent, action_id) ); diff --git a/pkg/code/lawenforcement/anti_money_laundering_test.go b/pkg/code/lawenforcement/anti_money_laundering_test.go index 59a13197..918722fd 100644 --- a/pkg/code/lawenforcement/anti_money_laundering_test.go +++ b/pkg/code/lawenforcement/anti_money_laundering_test.go @@ -21,7 +21,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/user/identity" currency_lib "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/kin" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/testutil" ) @@ -458,7 +457,7 @@ func makeReceivePaymentsPubliclyIntent(t *testing.T, phoneNumber string, owner * func setupPrivateBalance(t *testing.T, env amlTestEnv, owner *common.Account, balance uint64) { authority := testutil.NewRandomAccount(t) - timelockAccounts, err := authority.GetTimelockAccounts(timelock_token.DataVersion1, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -468,7 +467,7 @@ func setupPrivateBalance(t *testing.T, env amlTestEnv, owner *common.Account, ba OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: authority.PublicKey().ToBase58(), TokenAccount: timelockRecord.VaultAddress, - MintAccount: timelockRecord.Mint, + MintAccount: timelockAccounts.Mint.PublicKey().ToBase58(), AccountType: commonpb.AccountType_BUCKET_1_KIN, } require.NoError(t, env.data.CreateAccountInfo(env.ctx, &accountInfoRecord)) diff --git a/pkg/code/push/notifications.go b/pkg/code/push/notifications.go index ccc361fb..42aff9be 100644 --- a/pkg/code/push/notifications.go +++ b/pkg/code/push/notifications.go @@ -320,15 +320,15 @@ func SendChatMessagePushNotification( for _, content := range chatMessage.Content { var contentToPush *chatpb.Content switch typedContent := content.Type.(type) { - case *chatpb.Content_Localized: - localizedPushBody, err := localization.Localize(locale, typedContent.Localized.KeyOrText) + case *chatpb.Content_ServerLocalized: + localizedPushBody, err := localization.Localize(locale, typedContent.ServerLocalized.KeyOrText) if err != nil { continue } contentToPush = &chatpb.Content{ - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localizedPushBody, }, }, @@ -358,8 +358,8 @@ func SendChatMessagePushNotification( } contentToPush = &chatpb.Content{ - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localizedPushBody, }, }, diff --git a/pkg/code/server/grpc/account/server.go b/pkg/code/server/grpc/account/server.go index 172d211d..87cf949c 100644 --- a/pkg/code/server/grpc/account/server.go +++ b/pkg/code/server/grpc/account/server.go @@ -231,14 +231,6 @@ func (s *server) GetTokenAccountInfos(ctx context.Context, req *accountpb.GetTok return nil, status.Error(codes.Internal, "") } - legacyPrimary2022Records, err := common.GetLegacyPrimary2022AccountRecordsIfNotMigrated(ctx, s.data, owner) - if err != common.ErrNoPrivacyMigration2022 && err != nil { - log.WithError(err).Warn("failure getting legacy 2022 account records") - return nil, status.Error(codes.Internal, "") - } else if err == nil { - recordsByType[commonpb.AccountType_LEGACY_PRIMARY_2022] = []*common.AccountRecords{legacyPrimary2022Records} - } - // Trigger a deposit sync with the blockchain for the primary account, if it exists if primaryRecords, ok := recordsByType[commonpb.AccountType_PRIMARY]; ok { if !primaryRecords[0].General.RequiresDepositSync { @@ -441,18 +433,6 @@ func (s *server) getProtoAccountInfo(ctx context.Context, records *common.Accoun default: managementState = accountpb.TokenAccountInfo_MANAGEMENT_STATE_UNKNOWN } - - // Should never happen and is a precautionary check. We can't manage timelock - // accounts where we aren't the time authority. - if records.Timelock.TimeAuthority != common.GetSubsidizer().PublicKey().ToBase58() { - managementState = accountpb.TokenAccountInfo_MANAGEMENT_STATE_NONE - } - - // Should never happen and is a precautionary check. We can't manage timelock - // accounts where we aren't the close authority. - if records.Timelock.CloseAuthority != common.GetSubsidizer().PublicKey().ToBase58() { - managementState = accountpb.TokenAccountInfo_MANAGEMENT_STATE_NONE - } } blockchainState := accountpb.TokenAccountInfo_BLOCKCHAIN_STATE_DOES_NOT_EXIST diff --git a/pkg/code/server/grpc/account/server_test.go b/pkg/code/server/grpc/account/server_test.go index a1274a79..1852ee8c 100644 --- a/pkg/code/server/grpc/account/server_test.go +++ b/pkg/code/server/grpc/account/server_test.go @@ -17,14 +17,12 @@ import ( accountpb "github.com/code-payments/code-protobuf-api/generated/go/account/v1" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - "github.com/code-payments/code-server/pkg/code/balance" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/deposit" "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/payment" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/code/data/transaction" "github.com/code-payments/code-server/pkg/code/data/user" @@ -95,54 +93,8 @@ func TestIsCodeAccount_HappyPath(t *testing.T) { assert.Equal(t, accountpb.IsCodeAccountResponse_OK, resp.Result) } -func TestIsCodeAccount_LegacyPrimary2022Migration_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - req := &accountpb.IsCodeAccountRequest{ - Owner: ownerAccount.ToProto(), - } - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), - } - - resp, err := env.client.IsCodeAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.IsCodeAccountResponse_NOT_FOUND, resp.Result) - - legacyAccountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_LEGACY_PRIMARY_2022) - - resp, err = env.client.IsCodeAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.IsCodeAccountResponse_OK, resp.Result) - - setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_PRIMARY) - - resp, err = env.client.IsCodeAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.IsCodeAccountResponse_OK, resp.Result) - - setupPrivacyMigration2022Intent(t, env, ownerAccount) - - resp, err = env.client.IsCodeAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.IsCodeAccountResponse_OK, resp.Result) - - legacyAccountRecords.Timelock.VaultState = timelock_token_v1.StateClosed - legacyAccountRecords.Timelock.Block += 1 - require.NoError(t, env.data.SaveTimelock(env.ctx, legacyAccountRecords.Timelock)) - - resp, err = env.client.IsCodeAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.IsCodeAccountResponse_OK, resp.Result) -} - func TestIsCodeAccount_NotManagedByCode(t *testing.T) { - for i := 0; i < 5; i++ { + for i := 0; i < 4; i++ { for _, unmanagedState := range []timelock_token_v1.TimelockState{ timelock_token_v1.StateWaitingForTimeout, timelock_token_v1.StateUnlocked, @@ -166,7 +118,6 @@ func TestIsCodeAccount_NotManagedByCode(t *testing.T) { assert.Equal(t, accountpb.IsCodeAccountResponse_NOT_FOUND, resp.Result) var allAccountRecords []*common.AccountRecords - allAccountRecords = append(allAccountRecords, setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_LEGACY_PRIMARY_2022)) allAccountRecords = append(allAccountRecords, setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_PRIMARY)) allAccountRecords = append(allAccountRecords, setupAccountRecords(t, env, ownerAccount, testutil.NewRandomAccount(t), 0, commonpb.AccountType_BUCKET_100_KIN)) allAccountRecords = append(allAccountRecords, setupAccountRecords(t, env, ownerAccount, testutil.NewRandomAccount(t), 0, commonpb.AccountType_TEMPORARY_INCOMING)) @@ -242,7 +193,7 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { tokenAccount, err = authority.ToAssociatedTokenAccount(common.UsdcMintAccount) require.NoError(t, err) } else { - timelockAccounts, err := authority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) require.NoError(t, err) tokenAccount = timelockAccounts.Vault } @@ -443,7 +394,7 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { } { phoneNumber := fmt.Sprintf("+1800555%d", i) ownerAccount := testutil.NewRandomAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) require.NoError(t, err) req := &accountpb.GetTokenAccountInfosRequest{ @@ -628,65 +579,37 @@ func TestGetTokenAccountInfos_ManagementState(t *testing.T) { defer cleanup() for _, tc := range []struct { - timelockState timelock_token_v1.TimelockState - block uint64 - timeAuthority *common.Account - closeAuthority *common.Account - expected accountpb.TokenAccountInfo_ManagementState + timelockState timelock_token_v1.TimelockState + block uint64 + expected accountpb.TokenAccountInfo_ManagementState }{ { - timelockState: timelock_token_v1.StateUnknown, - block: 0, - timeAuthority: env.subsidizer, - closeAuthority: env.subsidizer, - expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_LOCKED, + timelockState: timelock_token_v1.StateUnknown, + block: 0, + expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_LOCKED, }, {timelockState: timelock_token_v1.StateUnknown, - block: 1, - timeAuthority: env.subsidizer, - closeAuthority: env.subsidizer, - expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_UNKNOWN, + block: 1, + expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_UNKNOWN, }, {timelockState: timelock_token_v1.StateUnlocked, - block: 2, - timeAuthority: env.subsidizer, - closeAuthority: env.subsidizer, - expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_UNLOCKED, + block: 2, + expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_UNLOCKED, }, { - timelockState: timelock_token_v1.StateWaitingForTimeout, - block: 3, - timeAuthority: env.subsidizer, - closeAuthority: env.subsidizer, - expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_UNLOCKING, + timelockState: timelock_token_v1.StateWaitingForTimeout, + block: 3, + expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_UNLOCKING, }, { - timelockState: timelock_token_v1.StateLocked, - block: 4, - timeAuthority: env.subsidizer, - closeAuthority: env.subsidizer, - expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_LOCKED, + timelockState: timelock_token_v1.StateLocked, + block: 4, + expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_LOCKED, }, { - timelockState: timelock_token_v1.StateClosed, - block: 5, - timeAuthority: env.subsidizer, - closeAuthority: env.subsidizer, - expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_CLOSED, - }, - { - timelockState: timelock_token_v1.StateLocked, - block: 6, - timeAuthority: testutil.NewRandomAccount(t), - closeAuthority: env.subsidizer, - expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_NONE, - }, - { - timelockState: timelock_token_v1.StateLocked, - block: 7, - timeAuthority: env.subsidizer, - closeAuthority: testutil.NewRandomAccount(t), - expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_NONE, + timelockState: timelock_token_v1.StateClosed, + block: 5, + expected: accountpb.TokenAccountInfo_MANAGEMENT_STATE_CLOSED, }, } { ownerAccount := testutil.NewRandomAccount(t) @@ -703,8 +626,6 @@ func TestGetTokenAccountInfos_ManagementState(t *testing.T) { accountRecords := getDefaultTestAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_PRIMARY) accountRecords.Timelock.VaultState = tc.timelockState accountRecords.Timelock.Block = tc.block - accountRecords.Timelock.TimeAuthority = tc.timeAuthority.PublicKey().ToBase58() - accountRecords.Timelock.CloseAuthority = tc.closeAuthority.PublicKey().ToBase58() require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountRecords.General)) require.NoError(t, env.data.SaveTimelock(env.ctx, accountRecords.Timelock)) @@ -792,112 +713,6 @@ func TestGetTokenAccountInfos_NoTokenAccounts(t *testing.T) { assert.Empty(t, resp.TokenAccountInfos) } -func TestGetTokenAccountInfos_LegacyPrimary2022Migration_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - req := &accountpb.GetTokenAccountInfosRequest{ - Owner: ownerAccount.ToProto(), - } - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), - } - - accountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_LEGACY_PRIMARY_2022) - setupCachedBalance(t, env, accountRecords, kin.ToQuarks(123)) - - resp, err := env.client.GetTokenAccountInfos(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 1) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token_v1.DataVersionLegacy, common.KinMintAccount) - require.NoError(t, err) - - accountInfo, ok := resp.TokenAccountInfos[timelockAccounts.Vault.PublicKey().ToBase58()] - require.True(t, ok) - - assert.Equal(t, commonpb.AccountType_LEGACY_PRIMARY_2022, accountInfo.AccountType) - assert.EqualValues(t, 0, accountInfo.Index) - assert.Equal(t, timelockAccounts.Vault.PublicKey().ToBytes(), accountInfo.Address.Value) - assert.Equal(t, ownerAccount.PublicKey().ToBytes(), accountInfo.Owner.Value) - assert.Equal(t, ownerAccount.PublicKey().ToBytes(), accountInfo.Authority.Value) - assert.Equal(t, common.KinMintAccount.PublicKey().ToBytes(), accountInfo.Mint.Value) - assert.Equal(t, accountpb.TokenAccountInfo_BALANCE_SOURCE_CACHE, accountInfo.BalanceSource) - assert.EqualValues(t, kin.ToQuarks(123), accountInfo.Balance) - assert.Equal(t, accountpb.TokenAccountInfo_MANAGEMENT_STATE_LOCKED, accountInfo.ManagementState) - assert.Equal(t, accountpb.TokenAccountInfo_BLOCKCHAIN_STATE_EXISTS, accountInfo.BlockchainState) - assert.False(t, accountInfo.MustRotate) -} - -func TestGetTokenAccountInfos_LegacyPrimary2022Migration_IntentSubmitted(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - req := &accountpb.GetTokenAccountInfosRequest{ - Owner: ownerAccount.ToProto(), - } - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), - } - - accountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_LEGACY_PRIMARY_2022) - setupCachedBalance(t, env, accountRecords, kin.ToQuarks(123)) - - resp, err := env.client.GetTokenAccountInfos(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 1) - - setupPrivacyMigration2022Intent(t, env, ownerAccount) - - resp, err = env.client.GetTokenAccountInfos(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.GetTokenAccountInfosResponse_NOT_FOUND, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 0) -} - -func TestGetTokenAccountInfos_LegacyPrimary2022Migration_AccountClosed(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - req := &accountpb.GetTokenAccountInfosRequest{ - Owner: ownerAccount.ToProto(), - } - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), - } - - accountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_LEGACY_PRIMARY_2022) - setupCachedBalance(t, env, accountRecords, kin.ToQuarks(123)) - - resp, err := env.client.GetTokenAccountInfos(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 1) - - accountRecords.Timelock.VaultState = timelock_token_v1.StateClosed - accountRecords.Timelock.Block += 1 - require.NoError(t, env.data.SaveTimelock(env.ctx, accountRecords.Timelock)) - - resp, err = env.client.GetTokenAccountInfos(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, accountpb.GetTokenAccountInfosResponse_NOT_FOUND, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 0) -} - func TestLinkAdditionalAccounts_HappyPath(t *testing.T) { env, cleanup := setup(t) defer cleanup() @@ -1097,8 +912,12 @@ func TestUnauthenticatedRPC(t *testing.T) { func setupAccountRecords(t *testing.T, env testEnv, ownerAccount, authorityAccount *common.Account, index uint64, accountType commonpb.AccountType) *common.AccountRecords { accountRecords := getDefaultTestAccountRecords(t, env, ownerAccount, authorityAccount, index, accountType) - if accountType != commonpb.AccountType_LEGACY_PRIMARY_2022 { - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountRecords.General)) + require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountRecords.General)) + + if accountRecords.IsTimelock() { + accountRecords.Timelock.VaultState = timelock_token_v1.StateLocked + accountRecords.Timelock.Block += 1 + require.NoError(t, env.data.SaveTimelock(env.ctx, accountRecords.Timelock)) } if accountType == commonpb.AccountType_TEMPORARY_INCOMING { @@ -1113,23 +932,10 @@ func setupAccountRecords(t *testing.T, env testEnv, ownerAccount, authorityAccou require.NoError(t, env.data.PutAllActions(env.ctx, actionRecord)) } - if accountRecords.IsTimelock() { - accountRecords.Timelock.VaultState = timelock_token_v1.StateLocked - accountRecords.Timelock.Block += 1 - require.NoError(t, env.data.SaveTimelock(env.ctx, accountRecords.Timelock)) - } - return accountRecords } func getDefaultTestAccountRecords(t *testing.T, env testEnv, ownerAccount, authorityAccount *common.Account, index uint64, accountType commonpb.AccountType) *common.AccountRecords { - var dataVerstion timelock_token_v1.TimelockDataVersion - if accountType == commonpb.AccountType_LEGACY_PRIMARY_2022 { - dataVerstion = timelock_token_v1.DataVersionLegacy - } else { - dataVerstion = timelock_token_v1.DataVersion1 - } - var tokenAccount *common.Account var mintAccount *common.Account var timelockRecord *timelock.Record @@ -1143,7 +949,7 @@ func getDefaultTestAccountRecords(t *testing.T, env testEnv, ownerAccount, autho } else { mintAccount = common.KinMintAccount - timelockAccounts, err := authorityAccount.GetTimelockAccounts(dataVerstion, mintAccount) + timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, mintAccount) require.NoError(t, err) timelockRecord = timelockAccounts.ToDBRecord() @@ -1172,40 +978,16 @@ func getDefaultTestAccountRecords(t *testing.T, env testEnv, ownerAccount, autho } func setupCachedBalance(t *testing.T, env testEnv, accountRecords *common.AccountRecords, balance uint64) { - if accountRecords.Timelock.DataVersion == timelock_token_v1.DataVersionLegacy { - paymentRecord := &payment.Record{ - Source: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Destination: accountRecords.General.TokenAccount, - Quantity: balance, - - Rendezvous: "", - IsExternal: true, + depositRecord := &deposit.Record{ + Signature: fmt.Sprintf("txn%d", rand.Uint64()), + Destination: accountRecords.General.TokenAccount, + Amount: balance, + UsdMarketValue: 1, - TransactionId: fmt.Sprintf("txn%d", rand.Uint64()), - - ConfirmationState: transaction.ConfirmationFinalized, - - ExchangeCurrency: string(currency.KIN), - ExchangeRate: 1.0, - UsdMarketValue: 1.0, - - BlockId: 12345, - - CreatedAt: time.Now(), - } - require.NoError(t, env.data.CreatePayment(env.ctx, paymentRecord)) - } else { - depositRecord := &deposit.Record{ - Signature: fmt.Sprintf("txn%d", rand.Uint64()), - Destination: accountRecords.General.TokenAccount, - Amount: balance, - UsdMarketValue: 1, - - ConfirmationState: transaction.ConfirmationFinalized, - Slot: 12345, - } - require.NoError(t, env.data.SaveExternalDeposit(env.ctx, depositRecord)) + ConfirmationState: transaction.ConfirmationFinalized, + Slot: 12345, } + require.NoError(t, env.data.SaveExternalDeposit(env.ctx, depositRecord)) } func setupOpenAccountsIntent(t *testing.T, env testEnv, ownerAccount *common.Account) { @@ -1222,26 +1004,3 @@ func setupOpenAccountsIntent(t *testing.T, env testEnv, ownerAccount *common.Acc require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) } - -func setupPrivacyMigration2022Intent(t *testing.T, env testEnv, ownerAccount *common.Account) { - tokenAccount, err := ownerAccount.ToTimelockVault(timelock_token_v1.DataVersionLegacy, common.KinMintAccount) - require.NoError(t, err) - - balance, err := balance.CalculateFromCache(env.ctx, env.data, tokenAccount) - require.NoError(t, err) - - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.MigrateToPrivacy2022, - - InitiatorOwnerAccount: ownerAccount.PublicKey().ToBase58(), - - MigrateToPrivacy2022Metadata: &intent.MigrateToPrivacy2022Metadata{ - Quantity: balance, - }, - - State: intent.StatePending, - } - - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) -} diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/server.go index 362c0fd7..17c201ca 100644 --- a/pkg/code/server/grpc/chat/server.go +++ b/pkg/code/server/grpc/chat/server.go @@ -147,7 +147,7 @@ func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*ch } protoMetadata.Title = &chatpb.ChatMetadata_Localized{ - Localized: &chatpb.LocalizedContent{ + Localized: &chatpb.ServerLocalizedContent{ KeyOrText: localization.LocalizeWithFallback( locale, localization.GetLocalizationKeyForUserAgent(ctx, chatProperties.TitleLocalizationKey), @@ -298,11 +298,11 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest for _, content := range protoChatMessage.Content { switch typed := content.Type.(type) { - case *chatpb.Content_Localized: - typed.Localized.KeyOrText = localization.LocalizeWithFallback( + case *chatpb.Content_ServerLocalized: + typed.ServerLocalized.KeyOrText = localization.LocalizeWithFallback( locale, - localization.GetLocalizationKeyForUserAgent(ctx, typed.Localized.KeyOrText), - typed.Localized.KeyOrText, + localization.GetLocalizationKeyForUserAgent(ctx, typed.ServerLocalized.KeyOrText), + typed.ServerLocalized.KeyOrText, ) } } diff --git a/pkg/code/server/grpc/chat/server_test.go b/pkg/code/server/grpc/chat/server_test.go index f6dd6eff..627764b0 100644 --- a/pkg/code/server/grpc/chat/server_test.go +++ b/pkg/code/server/grpc/chat/server_test.go @@ -102,8 +102,8 @@ func TestGetChatsAndMessages_HappyPath(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: "msg.body.key", }, }, @@ -242,7 +242,7 @@ func TestGetChatsAndMessages_HappyPath(t *testing.T) { require.Len(t, getMessagesResp.Messages, 1) assert.Equal(t, expectedCodeTeamMessage.MessageId.Value, getMessagesResp.Messages[0].Cursor.Value) getMessagesResp.Messages[0].Cursor = nil - expectedCodeTeamMessage.Content[0].GetLocalized().KeyOrText = "localized message body content" + expectedCodeTeamMessage.Content[0].GetServerLocalized().KeyOrText = "localized message body content" assert.True(t, proto.Equal(expectedCodeTeamMessage, getMessagesResp.Messages[0])) getMessagesResp, err = env.client.GetMessages(env.ctx, getCashTransactionsMessagesReq) @@ -288,8 +288,8 @@ func TestChatHistoryReadState_HappyPath(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: fmt.Sprintf("msg.body.key%d", i), }, }, @@ -346,8 +346,8 @@ func TestChatHistoryReadState_NegativeProgress(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: fmt.Sprintf("msg.body.key%d", i), }, }, @@ -429,8 +429,8 @@ func TestChatHistoryReadState_MessageNotFound(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: "msg.body.key", }, }, @@ -743,8 +743,8 @@ func TestUnauthorizedAccess(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: "msg.body.key", }, }, diff --git a/pkg/code/server/grpc/messaging/server.go b/pkg/code/server/grpc/messaging/server.go index c89f69ce..85308c09 100644 --- a/pkg/code/server/grpc/messaging/server.go +++ b/pkg/code/server/grpc/messaging/server.go @@ -17,6 +17,7 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" + commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" "github.com/code-payments/code-server/pkg/cache" @@ -285,7 +286,7 @@ func (s *server) OpenMessageStreamWithKeepAlive(streamer messagingpb.Messaging_O err := streamer.Send(&messagingpb.OpenMessageStreamWithKeepAliveResponse{ ResponseOrPing: &messagingpb.OpenMessageStreamWithKeepAliveResponse_Ping{ - Ping: &messagingpb.ServerPing{ + Ping: &commonpb.ServerPing{ Timestamp: timestamppb.Now(), PingDelay: durationpb.New(messageStreamPingDelay), }, diff --git a/pkg/code/server/grpc/messaging/testutil.go b/pkg/code/server/grpc/messaging/testutil.go index f1e7cf97..4968ab2a 100644 --- a/pkg/code/server/grpc/messaging/testutil.go +++ b/pkg/code/server/grpc/messaging/testutil.go @@ -373,7 +373,7 @@ func (c *clientEnv) receiveMessagesInRealTime(t *testing.T, rendezvousKey *commo case *messagingpb.OpenMessageStreamWithKeepAliveResponse_Ping: require.NoError(t, streamer.streamWithKeepAlives.Send(&messagingpb.OpenMessageStreamWithKeepAliveRequest{ RequestOrPong: &messagingpb.OpenMessageStreamWithKeepAliveRequest_Pong{ - Pong: &messagingpb.ClientPong{ + Pong: &commonpb.ClientPong{ Timestamp: timestamppb.Now(), }, }, @@ -467,7 +467,7 @@ func (c *clientEnv) waitUntilStreamTerminationOrTimeout(t *testing.T, rendezvous if keepStreamAlive { require.NoError(t, streamer.streamWithKeepAlives.Send(&messagingpb.OpenMessageStreamWithKeepAliveRequest{ RequestOrPong: &messagingpb.OpenMessageStreamWithKeepAliveRequest_Pong{ - Pong: &messagingpb.ClientPong{ + Pong: &commonpb.ClientPong{ Timestamp: timestamppb.Now(), }, }, diff --git a/pkg/code/server/grpc/phone/server_test.go b/pkg/code/server/grpc/phone/server_test.go index 8b629694..1be6ebad 100644 --- a/pkg/code/server/grpc/phone/server_test.go +++ b/pkg/code/server/grpc/phone/server_test.go @@ -587,7 +587,7 @@ func TestGetAssociatedPhoneNumber_UnlockedTimelockAccount(t *testing.T) { LastVerifiedAt: time.Now(), })) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token.DataVersion1, common.KinMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() require.NoError(t, env.data.SaveTimelock(env.ctx, timelockRecord)) @@ -596,7 +596,7 @@ func TestGetAssociatedPhoneNumber_UnlockedTimelockAccount(t *testing.T) { OwnerAccount: timelockRecord.VaultOwner, AuthorityAccount: timelockRecord.VaultOwner, TokenAccount: timelockRecord.VaultAddress, - MintAccount: timelockRecord.Mint, + MintAccount: timelockAccounts.Mint.PublicKey().ToBase58(), AccountType: commonpb.AccountType_PRIMARY, } require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) diff --git a/pkg/code/server/grpc/transaction/v2/airdrop.go b/pkg/code/server/grpc/transaction/v2/airdrop.go index 5b12d88d..f003a424 100644 --- a/pkg/code/server/grpc/transaction/v2/airdrop.go +++ b/pkg/code/server/grpc/transaction/v2/airdrop.go @@ -2,7 +2,10 @@ package transaction_v2 import ( "context" + "crypto/sha256" + "fmt" + "github.com/mr-tron/base58/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -537,21 +540,6 @@ func (s *transactionServer) isFirstReceiveFromOtherCodeUser(ctx context.Context, return firstReceive.IntentId == intentToCheck, nil } -func GetOldAirdropIntentId(airdropType AirdropType, reference string) string { - return fmt.Sprintf("airdrop-%d-%s", airdropType, reference) -} - -// Consistent intent ID that maps to a 32 byte buffer -func GetNewAirdropIntentId(airdropType AirdropType, reference string) string { - old := GetOldAirdropIntentId(airdropType, reference) - hashed := sha256.Sum256([]byte(old)) - return base58.Encode(hashed[:]) -} - -func getAirdropCacheKey(owner *common.Account, airdropType AirdropType) string { - return fmt.Sprintf("%s:%d\n", owner.PublicKey().ToBase58(), airdropType) -} - */ func (s *transactionServer) mustLoadAirdropper(ctx context.Context) { @@ -584,6 +572,21 @@ func (s *transactionServer) mustLoadAirdropper(ctx context.Context) { } } +func GetOldAirdropIntentId(airdropType AirdropType, reference string) string { + return fmt.Sprintf("airdrop-%d-%s", airdropType, reference) +} + +// Consistent intent ID that maps to a 32 byte buffer +func GetNewAirdropIntentId(airdropType AirdropType, reference string) string { + old := GetOldAirdropIntentId(airdropType, reference) + hashed := sha256.Sum256([]byte(old)) + return base58.Encode(hashed[:]) +} + +func getAirdropCacheKey(owner *common.Account, airdropType AirdropType) string { + return fmt.Sprintf("%s:%d\n", owner.PublicKey().ToBase58(), airdropType) +} + func (t AirdropType) String() string { switch t { case AirdropTypeUnknown: diff --git a/pkg/code/server/grpc/user/server_test.go b/pkg/code/server/grpc/user/server_test.go index 5c07b344..328fdaef 100644 --- a/pkg/code/server/grpc/user/server_test.go +++ b/pkg/code/server/grpc/user/server_test.go @@ -465,7 +465,7 @@ func TestGetUser_UnlockedTimelockAccount(t *testing.T) { LastVerifiedAt: time.Now(), })) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(timelock_token.DataVersion1, common.KinMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() require.NoError(t, env.data.SaveTimelock(env.ctx, timelockRecord)) @@ -474,7 +474,7 @@ func TestGetUser_UnlockedTimelockAccount(t *testing.T) { OwnerAccount: timelockRecord.VaultOwner, AuthorityAccount: timelockRecord.VaultOwner, TokenAccount: timelockRecord.VaultAddress, - MintAccount: timelockRecord.Mint, + MintAccount: timelockAccounts.Mint.PublicKey().ToBase58(), AccountType: commonpb.AccountType_PRIMARY, } require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) From 1b916d77ad81dd452ed2696ac00aef74405e1201 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 10 Sep 2024 12:39:50 -0700 Subject: [PATCH 31/79] Pull latest protobuf APIs --- go.mod | 4 ++-- go.sum | 14 ++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index e452b1cf..cf50fe80 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,9 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.18.1-0.20240820154350-ad076084d5a4 + github.com/code-payments/code-protobuf-api v1.18.1-0.20240910193727-b555f4096d1b github.com/code-payments/code-vm-indexer v0.1.0 + github.com/dghubble/oauth1 v0.7.3 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 @@ -66,7 +67,6 @@ require ( github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dghubble/oauth1 v0.7.3 // indirect github.com/docker/cli v20.10.7+incompatible // indirect github.com/docker/docker v20.10.7+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect diff --git a/go.sum b/go.sum index 6738ffb3..93ffd3e6 100644 --- a/go.sum +++ b/go.sum @@ -121,18 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.6 h1:QCot0U+4Ar5SdSX4v955FORMsd3Qcf0ZgkoqlGJZzu0= -github.com/code-payments/code-protobuf-api v1.16.6/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240816181522-d676c469560e h1:I7bUSQWqDeJ1pjJo58MFFiBraocNXpxzKYT/ZQRAYK8= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240816181522-d676c469560e/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240820134923-fef9aa9ba43c h1:OT6lfsDtao85Yxosdkm5QpPMdwWp4uln63450iEvPn0= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240820134923-fef9aa9ba43c/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240820143954-42db9b3554ca h1:xRsh9a/PZyouBpo1IBwdrVnCZZRmvlyDQlS1OJKAr/w= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240820143954-42db9b3554ca/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240820144322-178f09e6cb66 h1:9FXVlKe7qYXv2s3YU3z+ptxbVFiPtIwPRehIlTTntPM= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240820144322-178f09e6cb66/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240820154350-ad076084d5a4 h1:f/dui6iRfeYm/59hsh3gC9UU016po77bp1TdLnBuzG8= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240820154350-ad076084d5a4/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240910193727-b555f4096d1b h1:I0SftAkOlHfL5sl4pQoxV83C2Rk9qufrvnhSk+YR7iA= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240910193727-b555f4096d1b/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/code-payments/code-vm-indexer v0.1.0 h1:XzBwFrZp1R+9POGF/zMy5o6/OCI2J+jGJ7qr4cL72rY= github.com/code-payments/code-vm-indexer v0.1.0/go.mod h1:LtXqlb7ub0mPUNKlCPJbsEDQrkZvWTPSRM5hTdHcqpM= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= From 1e450853d1e5cd07cae8a40852f782753cfbe531 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Tue, 10 Sep 2024 18:28:28 -0400 Subject: [PATCH 32/79] Update VM vixn amount arguments to uint64 (#178) --- go.mod | 2 +- go.sum | 4 ++-- pkg/code/async/sequencer/fulfillment_handler.go | 2 +- pkg/code/transaction/transaction.go | 4 ++-- .../cvm/virtual_instructions_relay_transfer_external.go | 6 +++--- .../cvm/virtual_instructions_relay_transfer_internal.go | 6 +++--- .../cvm/virtual_instructions_timelock_transfer_external.go | 6 +++--- .../cvm/virtual_instructions_timelock_transfer_internal.go | 6 +++--- .../cvm/virtual_instructions_timelock_transfer_relay.go | 6 +++--- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index cf50fe80..2ae2c35c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.18.1-0.20240910193727-b555f4096d1b + github.com/code-payments/code-protobuf-api v1.18.1-0.20240910221949-6bf7280b7f2b github.com/code-payments/code-vm-indexer v0.1.0 github.com/dghubble/oauth1 v0.7.3 github.com/emirpasic/gods v1.12.0 diff --git a/go.sum b/go.sum index 93ffd3e6..e6725023 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240910193727-b555f4096d1b h1:I0SftAkOlHfL5sl4pQoxV83C2Rk9qufrvnhSk+YR7iA= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240910193727-b555f4096d1b/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240910221949-6bf7280b7f2b h1:FfwF4bH1QrqJ5G175PYJgbT8fXx8WiHUFUQg9auYBBY= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240910221949-6bf7280b7f2b/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/code-payments/code-vm-indexer v0.1.0 h1:XzBwFrZp1R+9POGF/zMy5o6/OCI2J+jGJ7qr4cL72rY= github.com/code-payments/code-vm-indexer v0.1.0/go.mod h1:LtXqlb7ub0mPUNKlCPJbsEDQrkZvWTPSRM5hTdHcqpM= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 7184ef8e..3e12d72b 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -779,7 +779,7 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c treasuryPoolVault, destination, commitment, - uint32(commitmentRecord.Amount), // todo: assumes amount never overflows uint32 + commitmentRecord.Amount, transcript, recentRoot, ) diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index 97e3316f..cb943e38 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -179,7 +179,7 @@ func MakeInternalTransferWithAuthorityTransaction( source *common.TimelockAccounts, destination *common.Account, - kinAmountInQuarks uint32, + kinAmountInQuarks uint64, ) (solana.Transaction, error) { memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) @@ -240,7 +240,7 @@ func MakeInternalTreasuryAdvanceTransaction( treasuryPoolVault *common.Account, destination *common.Account, commitment *common.Account, - kinAmountInQuarks uint32, + kinAmountInQuarks uint64, transcript []byte, recentRoot []byte, ) (solana.Transaction, error) { diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go index 6b4049a5..6b272229 100644 --- a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go +++ b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go @@ -5,14 +5,14 @@ import ( ) const ( - RelayTransferExternalVirtrualInstructionDataSize = (4 + // amount + RelayTransferExternalVirtrualInstructionDataSize = (8 + // amount HashSize + // transcript HashSize + // recent_root HashSize) // commitment ) type RelayTransferExternalVirtualInstructionArgs struct { - Amount uint32 + Amount uint64 Transcript Hash RecentRoot Hash Commitment Hash @@ -29,7 +29,7 @@ func NewRelayTransferExternalVirtualInstructionCtor( var offset int data := make([]byte, RelayTransferExternalVirtrualInstructionDataSize) - putUint32(data, args.Amount, &offset) + putUint64(data, args.Amount, &offset) putHash(data, args.Transcript, &offset) putHash(data, args.RecentRoot, &offset) putHash(data, args.Commitment, &offset) diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go index 891114c5..6dc4bc7b 100644 --- a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go +++ b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go @@ -5,14 +5,14 @@ import ( ) const ( - RelayTransferInternalVirtrualInstructionDataSize = (4 + // amount + RelayTransferInternalVirtrualInstructionDataSize = (8 + // amount HashSize + // transcript HashSize + // recent_root HashSize) // commitment ) type RelayTransferInternalVirtualInstructionArgs struct { - Amount uint32 + Amount uint64 Transcript Hash RecentRoot Hash Commitment Hash @@ -29,7 +29,7 @@ func NewRelayTransferInternalVirtualInstructionCtor( var offset int data := make([]byte, RelayTransferInternalVirtrualInstructionDataSize) - putUint32(data, args.Amount, &offset) + putUint64(data, args.Amount, &offset) putHash(data, args.Transcript, &offset) putHash(data, args.RecentRoot, &offset) putHash(data, args.Commitment, &offset) diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go index 833115f8..1daddac2 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go @@ -9,12 +9,12 @@ import ( const ( TimelockTransferExternalVirtrualInstructionDataSize = (SignatureSize + // signature - 4) // amount + 8) // amount ) type TimelockTransferExternalVirtualInstructionArgs struct { TimelockBump uint8 - Amount uint32 + Amount uint64 Signature Signature } @@ -34,7 +34,7 @@ func NewTimelockTransferExternalVirtualInstructionCtor( var offset int data := make([]byte, TimelockTransferExternalVirtrualInstructionDataSize) putSignature(data, args.Signature, &offset) - putUint32(data, args.Amount, &offset) + putUint64(data, args.Amount, &offset) ixns := []solana.Instruction{ newKreMemoIxn(), diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go index 59ac781b..4d2ca2b6 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go @@ -9,12 +9,12 @@ import ( const ( TimelockTransferInternalVirtrualInstructionDataSize = (SignatureSize + // signature - 4) // amount + 8) // amount ) type TimelockTransferInternalVirtualInstructionArgs struct { TimelockBump uint8 - Amount uint32 + Amount uint64 Signature Signature } @@ -34,7 +34,7 @@ func NewTimelockTransferInternalVirtualInstructionCtor( var offset int data := make([]byte, TimelockTransferInternalVirtrualInstructionDataSize) putSignature(data, args.Signature, &offset) - putUint32(data, args.Amount, &offset) + putUint64(data, args.Amount, &offset) ixns := []solana.Instruction{ newKreMemoIxn(), diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go index 00a7d4eb..935b58e9 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go @@ -9,12 +9,12 @@ import ( const ( TimelockTransferRelayVirtrualInstructionDataSize = (SignatureSize + // signature - 4) // amount + 8) // amount ) type TimelockTransferRelayVirtualInstructionArgs struct { TimelockBump uint8 - Amount uint32 + Amount uint64 Signature Signature } @@ -34,7 +34,7 @@ func NewTimelockTransferRelayVirtualInstructionCtor( var offset int data := make([]byte, TimelockTransferRelayVirtrualInstructionDataSize) putSignature(data, args.Signature, &offset) - putUint32(data, args.Amount, &offset) + putUint64(data, args.Amount, &offset) ixns := []solana.Instruction{ newKreMemoIxn(), From 1e863ced539934fca44bd66a6a4a5837c5339810 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 11 Sep 2024 12:16:20 -0400 Subject: [PATCH 33/79] Implement simplest vixn packing (#177) * Implement simplest vixn packing (WIP) * Fixes for uint64 vixn amounts * Complete MakeOnDemandTransaction implementations --- .../async/sequencer/fulfillment_handler.go | 475 ++++++++++++++++-- pkg/code/async/sequencer/vm.go | 70 ++- pkg/code/async/sequencer/worker.go | 6 + pkg/code/common/vm.go | 5 + .../grpc/transaction/v2/intent_handler.go | 2 - pkg/code/transaction/transaction.go | 108 +++- 6 files changed, 577 insertions(+), 89 deletions(-) diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 3e12d72b..ad29a12d 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "github.com/mr-tron/base58" + commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" @@ -46,6 +48,10 @@ type FulfillmentHandler interface { // SupportsOnDemandTransactions returns whether a fulfillment type supports // on demand transaction creation + // + // Note: This is also being abused for an initial version of packing virtual + // instructions 1:1 into a Solana transaction. A new flow/strategy will be + // needed when we require more efficient packing. SupportsOnDemandTransactions() bool // MakeOnDemandTransaction constructs a transaction at the time of submission @@ -166,12 +172,6 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransact if err != nil { return nil, err } - - err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) - if err != nil { - return nil, err - } - return &txn, nil } @@ -203,12 +203,14 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) IsRevoked(ctx contex } type NoPrivacyTransferWithAuthorityFulfillmentHandler struct { - data code_data.Provider + data code_data.Provider + vmIndexerClient indexerpb.IndexerClient } -func NewNoPrivacyTransferWithAuthorityFulfillmentHandler(data code_data.Provider) FulfillmentHandler { +func NewNoPrivacyTransferWithAuthorityFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { return &NoPrivacyTransferWithAuthorityFulfillmentHandler{ - data: data, + data: data, + vmIndexerClient: vmIndexerClient, } } @@ -275,20 +277,116 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) IsRevoked(ctx context } func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false + return true } func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") + virtualSignatureBytes, err := base58.Decode(*fulfillmentRecord.VirtualSignature) + if err != nil { + return nil, err + } + + virtualNonce, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.VirtualNonce) + if err != nil { + return nil, err + } + + virtualBlockhashBytes, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) + if err != nil { + return nil, err + } + + actionRecord, err := h.data.GetActionById(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) + if err != nil { + return nil, err + } + + sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) + if err != nil { + return nil, err + } + + sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, sourceVault.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + + sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } + + sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + return nil, err + } + + destinationVault, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) + if err != nil { + return nil, err + } + + destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationVault.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + + destinationAuthority, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } + + _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) + if err != nil { + return nil, err + } + + _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, sourceAuthority) + if err != nil { + return nil, err + } + + _, destinationeMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationAuthority) + if err != nil { + return nil, err + } + + // todo: support external transfers + txn, err := transaction_util.MakeInternalTransferWithAuthorityTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + solana.Signature(virtualSignatureBytes), + virtualNonce, + solana.Blockhash(virtualBlockhashBytes), + + common.CodeVmAccount, + nonceMemory, + nonceIndex, + sourceMemory, + sourceIndex, + destinationeMemory, + destinationIndex, + + sourceTimelockAccounts, + destinationVault, + *actionRecord.Quantity, + ) + if err != nil { + return nil, err + } + return &txn, nil } type NoPrivacyWithdrawFulfillmentHandler struct { - data code_data.Provider + data code_data.Provider + vmIndexerClient indexerpb.IndexerClient } -func NewNoPrivacyWithdrawFulfillmentHandler(data code_data.Provider) FulfillmentHandler { +func NewNoPrivacyWithdrawFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { return &NoPrivacyWithdrawFulfillmentHandler{ - data: data, + data: data, + vmIndexerClient: vmIndexerClient, } } @@ -343,11 +441,99 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) CanSubmitToBlockchain(ctx context. } func (h *NoPrivacyWithdrawFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false + return true } func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") + virtualSignatureBytes, err := base58.Decode(*fulfillmentRecord.VirtualSignature) + if err != nil { + return nil, err + } + + virtualNonce, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.VirtualNonce) + if err != nil { + return nil, err + } + + virtualBlockhashBytes, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) + if err != nil { + return nil, err + } + + sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) + if err != nil { + return nil, err + } + + sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, sourceVault.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + + sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } + + sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + return nil, err + } + + destinationVault, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) + if err != nil { + return nil, err + } + + destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationVault.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + + destinationAuthority, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } + + _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) + if err != nil { + return nil, err + } + + _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, sourceAuthority) + if err != nil { + return nil, err + } + + _, destinationeMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationAuthority) + if err != nil { + return nil, err + } + + // todo: support external transfers + txn, err := transaction_util.MakeInternalWithdrawTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + solana.Signature(virtualSignatureBytes), + virtualNonce, + solana.Blockhash(virtualBlockhashBytes), + + common.CodeVmAccount, + nonceMemory, + nonceIndex, + sourceMemory, + sourceIndex, + destinationeMemory, + destinationIndex, + + sourceTimelockAccounts, + destinationVault, + ) + if err != nil { + return nil, err + } + return &txn, nil } func (h *NoPrivacyWithdrawFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { @@ -381,14 +567,16 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) IsRevoked(ctx context.Context, ful } type TemporaryPrivacyTransferWithAuthorityFulfillmentHandler struct { - conf *conf - data code_data.Provider + conf *conf + data code_data.Provider + vmIndexerClient indexerpb.IndexerClient } -func NewTemporaryPrivacyTransferWithAuthorityFulfillmentHandler(data code_data.Provider, configProvider ConfigProvider) FulfillmentHandler { +func NewTemporaryPrivacyTransferWithAuthorityFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) FulfillmentHandler { return &TemporaryPrivacyTransferWithAuthorityFulfillmentHandler{ - conf: configProvider(), - data: data, + conf: configProvider(), + data: data, + vmIndexerClient: vmIndexerClient, } } @@ -452,11 +640,108 @@ func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) CanSubmitToBlo } func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false + return true } func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") + virtualSignatureBytes, err := base58.Decode(*fulfillmentRecord.VirtualSignature) + if err != nil { + return nil, err + } + + virtualNonce, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.VirtualNonce) + if err != nil { + return nil, err + } + + virtualBlockhashBytes, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) + if err != nil { + return nil, err + } + + commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) + if err != nil { + return nil, err + } + + treasuryPool, err := common.NewAccountFromPrivateKeyString(commitmentRecord.Pool) + if err != nil { + return nil, err + } + + treasuryPoolVault, err := common.NewAccountFromPrivateKeyString(*fulfillmentRecord.Destination) + if err != nil { + return nil, err + } + + commitmentVault, err := common.NewAccountFromPublicKeyString(commitmentRecord.VaultAddress) + if err != nil { + return nil, err + } + + sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) + if err != nil { + return nil, err + } + + sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, sourceVault.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + + sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } + + sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + return nil, err + } + + _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) + if err != nil { + return nil, err + } + + _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, sourceAuthority) + if err != nil { + return nil, err + } + + _, relayMemory, relayIndex, err := getVirtualRelayAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, commitmentVault) + if err != nil { + return nil, err + } + + txn, err := transaction_util.MakeCashChequeTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + solana.Signature(virtualSignatureBytes), + virtualNonce, + solana.Blockhash(virtualBlockhashBytes), + + common.CodeVmAccount, + common.CodeVmOmnibusAccount, + + nonceMemory, + nonceIndex, + sourceMemory, + sourceIndex, + relayMemory, + relayIndex, + + sourceTimelockAccounts, + treasuryPool, + treasuryPoolVault, + commitmentVault, + commitmentRecord.Amount, + ) + if err != nil { + return nil, err + } + return &txn, nil } func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { @@ -514,14 +799,16 @@ func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) IsRevoked(ctx } type PermanentPrivacyTransferWithAuthorityFulfillmentHandler struct { - conf *conf - data code_data.Provider + conf *conf + data code_data.Provider + vmIndexerClient indexerpb.IndexerClient } -func NewPermanentPrivacyTransferWithAuthorityFulfillmentHandler(data code_data.Provider, configProvider ConfigProvider) FulfillmentHandler { +func NewPermanentPrivacyTransferWithAuthorityFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) FulfillmentHandler { return &PermanentPrivacyTransferWithAuthorityFulfillmentHandler{ - conf: configProvider(), - data: data, + conf: configProvider(), + data: data, + vmIndexerClient: vmIndexerClient, } } @@ -575,11 +862,113 @@ func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) CanSubmitToBlo } func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false + return true } func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") + virtualSignatureBytes, err := base58.Decode(*fulfillmentRecord.VirtualSignature) + if err != nil { + return nil, err + } + + virtualNonce, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.VirtualNonce) + if err != nil { + return nil, err + } + + virtualBlockhashBytes, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) + if err != nil { + return nil, err + } + + oldCommitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) + if err != nil { + return nil, err + } + + newCommitmentRecord, err := h.data.GetCommitmentByVault(ctx, *oldCommitmentRecord.RepaymentDivertedTo) + if err != nil { + return nil, err + } + + treasuryPool, err := common.NewAccountFromPrivateKeyString(newCommitmentRecord.Pool) + if err != nil { + return nil, err + } + + treasuryPoolVault, err := common.NewAccountFromPrivateKeyString(*fulfillmentRecord.Destination) + if err != nil { + return nil, err + } + + commitmentVault, err := common.NewAccountFromPublicKeyString(newCommitmentRecord.VaultAddress) + if err != nil { + return nil, err + } + + sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) + if err != nil { + return nil, err + } + + sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, sourceVault.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + + sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } + + sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + return nil, err + } + + _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) + if err != nil { + return nil, err + } + + _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, sourceAuthority) + if err != nil { + return nil, err + } + + _, relayMemory, relayIndex, err := getVirtualRelayAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, commitmentVault) + if err != nil { + return nil, err + } + + txn, err := transaction_util.MakeCashChequeTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + solana.Signature(virtualSignatureBytes), + virtualNonce, + solana.Blockhash(virtualBlockhashBytes), + + common.CodeVmAccount, + common.CodeVmOmnibusAccount, + + nonceMemory, + nonceIndex, + sourceMemory, + sourceIndex, + relayMemory, + relayIndex, + + sourceTimelockAccounts, + treasuryPool, + treasuryPoolVault, + commitmentVault, + oldCommitmentRecord.Amount, + ) + if err != nil { + return nil, err + } + return &txn, nil } func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { @@ -786,12 +1175,6 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c if err != nil { return nil, err } - - err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) - if err != nil { - return nil, err - } - return &txn, nil } @@ -918,12 +1301,6 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ct if err != nil { return nil, err } - - err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) - if err != nil { - return nil, err - } - return &txn, nil } @@ -1086,12 +1463,6 @@ func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context. if err != nil { return nil, err } - - err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) - if err != nil { - return nil, err - } - return &txn, nil } @@ -1188,10 +1559,10 @@ func estimateTreasuryPoolFundingLevels(ctx context.Context, data code_data.Provi func getFulfillmentHandlers(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) map[fulfillment.Type]FulfillmentHandler { handlersByType := make(map[fulfillment.Type]FulfillmentHandler) handlersByType[fulfillment.InitializeLockedTimelockAccount] = NewInitializeLockedTimelockAccountFulfillmentHandler(data) - handlersByType[fulfillment.NoPrivacyTransferWithAuthority] = NewNoPrivacyTransferWithAuthorityFulfillmentHandler(data) - handlersByType[fulfillment.NoPrivacyWithdraw] = NewNoPrivacyWithdrawFulfillmentHandler(data) - handlersByType[fulfillment.TemporaryPrivacyTransferWithAuthority] = NewTemporaryPrivacyTransferWithAuthorityFulfillmentHandler(data, configProvider) - handlersByType[fulfillment.PermanentPrivacyTransferWithAuthority] = NewPermanentPrivacyTransferWithAuthorityFulfillmentHandler(data, configProvider) + handlersByType[fulfillment.NoPrivacyTransferWithAuthority] = NewNoPrivacyTransferWithAuthorityFulfillmentHandler(data, vmIndexerClient) + handlersByType[fulfillment.NoPrivacyWithdraw] = NewNoPrivacyWithdrawFulfillmentHandler(data, vmIndexerClient) + handlersByType[fulfillment.TemporaryPrivacyTransferWithAuthority] = NewTemporaryPrivacyTransferWithAuthorityFulfillmentHandler(data, vmIndexerClient, configProvider) + handlersByType[fulfillment.PermanentPrivacyTransferWithAuthority] = NewPermanentPrivacyTransferWithAuthorityFulfillmentHandler(data, vmIndexerClient, configProvider) handlersByType[fulfillment.TransferWithCommitment] = NewTransferWithCommitmentFulfillmentHandler(data, vmIndexerClient) handlersByType[fulfillment.CloseEmptyTimelockAccount] = NewCloseEmptyTimelockAccountFulfillmentHandler(data, vmIndexerClient) handlersByType[fulfillment.SaveRecentRoot] = NewSaveRecentRootFulfillmentHandler(data) diff --git a/pkg/code/async/sequencer/vm.go b/pkg/code/async/sequencer/vm.go index 9fd849da..ca505eae 100644 --- a/pkg/code/async/sequencer/vm.go +++ b/pkg/code/async/sequencer/vm.go @@ -82,28 +82,60 @@ func getVirtualTimelockAccountStateInMemory(ctx context.Context, vmIndexerClient if len(resp.Items) > 1 { return nil, nil, 0, errors.New("multiple results returned") - } else if resp.Items[0].Storage.GetCompressed() != nil { + } else if resp.Items[0].Storage.GetMemory() == nil { return nil, nil, 0, errors.New("account is compressed") } - memory, err := common.NewAccountFromPublicKeyBytes(resp.Items[0].Storage.GetMemory().Account.Value) + protoMemory := resp.Items[0].Storage.GetMemory() + memory, err := common.NewAccountFromPublicKeyBytes(protoMemory.Account.Value) if err != nil { return nil, nil, 0, err } + protoAccount := resp.Items[0].Account state := cvm.VirtualTimelockAccount{ - Owner: resp.Items[0].Account.Owner.Value, - Nonce: cvm.Hash(resp.Items[0].Account.Nonce.Value), + Owner: protoAccount.Owner.Value, + Nonce: cvm.Hash(protoAccount.Nonce.Value), - TokenBump: uint8(resp.Items[0].Account.TokenBump), - UnlockBump: uint8(resp.Items[0].Account.UnlockBump), - WithdrawBump: uint8(resp.Items[0].Account.WithdrawBump), + TokenBump: uint8(protoAccount.TokenBump), + UnlockBump: uint8(protoAccount.UnlockBump), + WithdrawBump: uint8(protoAccount.WithdrawBump), - Balance: resp.Items[0].Account.Balance, - Bump: uint8(resp.Items[0].Account.Bump), + Balance: protoAccount.Balance, + Bump: uint8(protoAccount.Bump), } - return &state, memory, uint16(resp.Items[0].Storage.GetMemory().Index), nil + return &state, memory, uint16(protoMemory.Index), nil +} + +func getVirtualDurableNonceAccountStateInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, nonce *common.Account) (*cvm.VirtualDurableNonce, *common.Account, uint16, error) { + resp, err := vmIndexerClient.GetVirtualDurableNonce(ctx, &indexerpb.GetVirtualDurableNonceRequest{ + VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()}, + Address: &indexerpb.Address{Value: nonce.PublicKey().ToBytes()}, + }) + if err != nil { + return nil, nil, 0, err + } else if resp.Result != indexerpb.GetVirtualDurableNonceResponse_OK { + return nil, nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) + } + + protoMemory := resp.Item.Storage.GetMemory() + if protoMemory == nil { + return nil, nil, 0, errors.New("account is compressed") + } + + memory, err := common.NewAccountFromPublicKeyBytes(protoMemory.Account.Value) + if err != nil { + return nil, nil, 0, err + } + + protoAccount := resp.Item.Account + state := cvm.VirtualDurableNonce{ + Address: protoAccount.Address.Value, + Nonce: cvm.Hash(protoAccount.Nonce.Value), + } + + return &state, memory, uint16(protoMemory.Index), nil } func getVirtualRelayAccountStateInMemory(ctx context.Context, vmIndexerClient indexerpb.IndexerClient, vm, relay *common.Account) (*cvm.VirtualRelayAccount, *common.Account, uint16, error) { @@ -117,17 +149,23 @@ func getVirtualRelayAccountStateInMemory(ctx context.Context, vmIndexerClient in return nil, nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) } - memory, err := common.NewAccountFromPublicKeyBytes(resp.Item.Storage.GetMemory().Account.Value) + protoMemory := resp.Item.Storage.GetMemory() + if protoMemory == nil { + return nil, nil, 0, errors.New("account is compressed") + } + + memory, err := common.NewAccountFromPublicKeyBytes(protoMemory.Account.Value) if err != nil { return nil, nil, 0, err } + protoAccount := resp.Item.Account state := cvm.VirtualRelayAccount{ - Address: resp.Item.Account.Address.Value, - Commitment: cvm.Hash(resp.Item.Account.Commitment.Value), - RecentRoot: cvm.Hash(resp.Item.Account.RecentRoot.Value), - Destination: resp.Item.Account.Destination.Value, + Address: protoAccount.Address.Value, + Commitment: cvm.Hash(protoAccount.Commitment.Value), + RecentRoot: cvm.Hash(protoAccount.RecentRoot.Value), + Destination: protoAccount.Destination.Value, } - return &state, memory, uint16(resp.Item.Storage.GetMemory().Index), nil + return &state, memory, uint16(protoMemory.Index), nil } diff --git a/pkg/code/async/sequencer/worker.go b/pkg/code/async/sequencer/worker.go index b42d8b01..081339d9 100644 --- a/pkg/code/async/sequencer/worker.go +++ b/pkg/code/async/sequencer/worker.go @@ -10,6 +10,7 @@ import ( "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkg/errors" + "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/transaction" @@ -239,6 +240,11 @@ func (p *service) handlePending(ctx context.Context, record *fulfillment.Record) return err } + err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) + if err != nil { + return err + } + record.Signature = pointer.String(base58.Encode(txn.Signature())) record.Nonce = pointer.String(selectedNonce.Account.PublicKey().ToBase58()) record.Blockhash = pointer.String(base58.Encode(selectedNonce.Blockhash[:])) diff --git a/pkg/code/common/vm.go b/pkg/code/common/vm.go index 5c2cdc5b..37f29918 100644 --- a/pkg/code/common/vm.go +++ b/pkg/code/common/vm.go @@ -5,4 +5,9 @@ var ( // // todo: real public key once program is deployed and VM instance is initialized CodeVmAccount, _ = NewAccountFromPublicKeyString("BkwoMG33cgSDrc3fEjfhZufqzYC3icXTTMajuueXyYGG") + + // The well-known Code VM instance omnibus used by the Code app + // + // todo: real public key once program is deployed and VM omnibus instance is initialized + CodeVmOmnibusAccount, _ = NewAccountFromPublicKeyString("SqKpQBYg8H69c8dusmSoLsya281cqhfzDVR2EJFHy1P") ) diff --git a/pkg/code/server/grpc/transaction/v2/intent_handler.go b/pkg/code/server/grpc/transaction/v2/intent_handler.go index baaf645c..252d33a3 100644 --- a/pkg/code/server/grpc/transaction/v2/intent_handler.go +++ b/pkg/code/server/grpc/transaction/v2/intent_handler.go @@ -35,8 +35,6 @@ import ( push_lib "github.com/code-payments/code-server/pkg/push" ) -// todo: Make working with different timelock versions easier - var accountTypesToOpen = []commonpb.AccountType{ commonpb.AccountType_PRIMARY, commonpb.AccountType_TEMPORARY_INCOMING, diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index cb943e38..cbef6ab5 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -8,7 +8,6 @@ import ( "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/cvm" - "github.com/code-payments/code-server/pkg/solana/memo" ) // todo: The argument sizes are blowing out of proportion, though there's likely @@ -89,7 +88,7 @@ func MakeCompressAccountTransaction( return MakeNoncedTransaction(nonce, bh, compressInstruction) } -func MakeInternalCloseAccountWithBalanceTransaction( +func MakeInternalWithdrawTransaction( nonce *common.Account, bh solana.Blockhash, @@ -107,14 +106,12 @@ func MakeInternalCloseAccountWithBalanceTransaction( source *common.TimelockAccounts, destination *common.Account, - - additionalMemo *string, ) (solana.Transaction, error) { memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) memoryCPublicKeyBytes := ed25519.PublicKey(destinationMemory.PublicKey().ToBytes()) - transferVirtualIxn := cvm.NewVirtualInstruction( + withdrawVirtualIxn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), &cvm.VirtualDurableNonce{ Address: virtualNonce.PublicKey().ToBytes(), @@ -145,20 +142,14 @@ func MakeInternalCloseAccountWithBalanceTransaction( VmMemC: &memoryCPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ - Opcode: transferVirtualIxn.Opcode, + Opcode: withdrawVirtualIxn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, MemBanks: []uint8{0, 1, 2}, - Data: transferVirtualIxn.Data, + Data: withdrawVirtualIxn.Data, }, ) - var instructions []solana.Instruction - if additionalMemo != nil { - instructions = append(instructions, memo.Instruction(*additionalMemo)) - } - instructions = append(instructions, execInstruction) - - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, bh, execInstruction) } func MakeInternalTransferWithAuthorityTransaction( @@ -244,11 +235,12 @@ func MakeInternalTreasuryAdvanceTransaction( transcript []byte, recentRoot []byte, ) (solana.Transaction, error) { - relayPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) - relayVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) memoryAPublicKeyBytes := ed25519.PublicKey(accountMemory.PublicKey().ToBytes()) memoryBPublicKeyBytes := ed25519.PublicKey(relayMemory.PublicKey().ToBytes()) + treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) + treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) + relayTransferInternalVirtualInstruction := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), nil, @@ -268,9 +260,9 @@ func MakeInternalTreasuryAdvanceTransaction( VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemA: &memoryAPublicKeyBytes, - VmOmnibus: &memoryBPublicKeyBytes, - VmRelay: &relayPublicKeyBytes, - VmRelayVault: &relayVaultPublicKeyBytes, + VmMemB: &memoryBPublicKeyBytes, + VmRelay: &treasuryPoolPublicKeyBytes, + VmRelayVault: &treasuryPoolVaultPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ Opcode: relayTransferInternalVirtualInstruction.Opcode, @@ -282,3 +274,81 @@ func MakeInternalTreasuryAdvanceTransaction( return MakeNoncedTransaction(nonce, bh, execInstruction) } + +func MakeCashChequeTransaction( + nonce *common.Account, + bh solana.Blockhash, + + virtualSignature solana.Signature, + virtualNonce *common.Account, + virtualBlockhash solana.Blockhash, + + vm *common.Account, + vmOmnibus *common.Account, + + nonceMemory *common.Account, + nonceIndex uint16, + sourceMemory *common.Account, + sourceIndex uint16, + relayMemory *common.Account, + relayIndex uint16, + + source *common.TimelockAccounts, + treasuryPool *common.Account, + treasuryPoolVault *common.Account, + commitmentVault *common.Account, + kinAmountInQuarks uint64, +) (solana.Transaction, error) { + vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) + + memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) + memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) + memoryCPublicKeyBytes := ed25519.PublicKey(relayMemory.PublicKey().ToBytes()) + + treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) + treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) + + transferRelayVirtualIxn := cvm.NewVirtualInstruction( + common.GetSubsidizer().PublicKey().ToBytes(), + &cvm.VirtualDurableNonce{ + Address: virtualNonce.PublicKey().ToBytes(), + Nonce: cvm.Hash(virtualBlockhash), + }, + cvm.NewTimelockTransferRelayVirtualInstructionCtor( + &cvm.TimelockTransferRelayVirtualInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + VirtualTimelock: source.State.PublicKey().ToBytes(), + VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), + Owner: source.VaultOwner.PublicKey().ToBytes(), + RelayVault: commitmentVault.PublicKey().ToBytes(), + }, + &cvm.TimelockTransferRelayVirtualInstructionArgs{ + TimelockBump: source.StateBump, + Amount: kinAmountInQuarks, + Signature: cvm.Signature(virtualSignature), + }, + ), + ) + + execInstruction := cvm.NewVmExecInstruction( + &cvm.VmExecInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmMemA: &memoryAPublicKeyBytes, + VmMemB: &memoryBPublicKeyBytes, + VmMemC: &memoryCPublicKeyBytes, + VmOmnibus: &vmOmnibusPublicKeyBytes, + VmRelay: &treasuryPoolPublicKeyBytes, + VmRelayVault: &treasuryPoolVaultPublicKeyBytes, + ExternalAddress: &treasuryPoolVaultPublicKeyBytes, + }, + &cvm.VmExecInstructionArgs{ + Opcode: transferRelayVirtualIxn.Opcode, + MemIndices: []uint16{nonceIndex, sourceIndex, relayIndex}, + MemBanks: []uint8{0, 1, 2}, + Data: transferRelayVirtualIxn.Data, + }, + ) + + return MakeNoncedTransaction(nonce, bh, execInstruction) +} From e1070ba6fc9bcaf1cedddb68f6517f983fbfd21d Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 11 Sep 2024 16:33:00 -0400 Subject: [PATCH 34/79] Support external VM transfers (#179) --- .../async/sequencer/fulfillment_handler.go | 265 ++++++++++++------ pkg/code/async/sequencer/vm.go | 15 + .../grpc/transaction/v2/intent_handler.go | 17 +- pkg/code/transaction/transaction.go | 210 +++++++++++++- ...instructions_timelock_withdraw_external.go | 10 +- 5 files changed, 395 insertions(+), 122 deletions(-) diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index ad29a12d..ce2b4b90 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -321,17 +321,7 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti return nil, err } - destinationVault, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) - if err != nil { - return nil, err - } - - destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationVault.PublicKey().ToBase58()) - if err != nil { - return nil, err - } - - destinationAuthority, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) + destinationTokenAccount, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) if err != nil { return nil, err } @@ -346,34 +336,71 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti return nil, err } - _, destinationeMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationAuthority) + isInternal, err := isInternalVmTransfer(ctx, h.data, destinationTokenAccount) if err != nil { return nil, err } - // todo: support external transfers - txn, err := transaction_util.MakeInternalTransferWithAuthorityTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + var txn solana.Transaction + var makeTxnErr error + if isInternal { + destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationTokenAccount.PublicKey().ToBase58()) + if err != nil { + return nil, err + } - solana.Signature(virtualSignatureBytes), - virtualNonce, - solana.Blockhash(virtualBlockhashBytes), + destinationAuthority, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } - common.CodeVmAccount, - nonceMemory, - nonceIndex, - sourceMemory, - sourceIndex, - destinationeMemory, - destinationIndex, + _, destinationeMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationAuthority) + if err != nil { + return nil, err + } - sourceTimelockAccounts, - destinationVault, - *actionRecord.Quantity, - ) - if err != nil { - return nil, err + txn, makeTxnErr = transaction_util.MakeInternalTransferWithAuthorityTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + solana.Signature(virtualSignatureBytes), + virtualNonce, + solana.Blockhash(virtualBlockhashBytes), + + common.CodeVmAccount, + nonceMemory, + nonceIndex, + sourceMemory, + sourceIndex, + destinationeMemory, + destinationIndex, + + sourceTimelockAccounts, + destinationTokenAccount, + *actionRecord.Quantity, + ) + } else { + txn, makeTxnErr = transaction_util.MakeExternalTransferWithAuthorityTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + solana.Signature(virtualSignatureBytes), + virtualNonce, + solana.Blockhash(virtualBlockhashBytes), + + common.CodeVmAccount, + nonceMemory, + nonceIndex, + sourceMemory, + sourceIndex, + + sourceTimelockAccounts, + destinationTokenAccount, + *actionRecord.Quantity, + ) + } + if makeTxnErr != nil { + return nil, makeTxnErr } return &txn, nil } @@ -480,17 +507,7 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex return nil, err } - destinationVault, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) - if err != nil { - return nil, err - } - - destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationVault.PublicKey().ToBase58()) - if err != nil { - return nil, err - } - - destinationAuthority, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) + destinationTokenAccount, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) if err != nil { return nil, err } @@ -505,33 +522,69 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex return nil, err } - _, destinationeMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationAuthority) + isInternal, err := isInternalVmTransfer(ctx, h.data, destinationTokenAccount) if err != nil { return nil, err } - // todo: support external transfers - txn, err := transaction_util.MakeInternalWithdrawTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + var txn solana.Transaction + var makeTxnErr error + if isInternal { + destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationTokenAccount.PublicKey().ToBase58()) + if err != nil { + return nil, err + } - solana.Signature(virtualSignatureBytes), - virtualNonce, - solana.Blockhash(virtualBlockhashBytes), + destinationAuthority, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } - common.CodeVmAccount, - nonceMemory, - nonceIndex, - sourceMemory, - sourceIndex, - destinationeMemory, - destinationIndex, + _, destinationeMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationAuthority) + if err != nil { + return nil, err + } - sourceTimelockAccounts, - destinationVault, - ) - if err != nil { - return nil, err + txn, makeTxnErr = transaction_util.MakeInternalWithdrawTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + solana.Signature(virtualSignatureBytes), + virtualNonce, + solana.Blockhash(virtualBlockhashBytes), + + common.CodeVmAccount, + nonceMemory, + nonceIndex, + sourceMemory, + sourceIndex, + destinationeMemory, + destinationIndex, + + sourceTimelockAccounts, + destinationTokenAccount, + ) + } else { + txn, makeTxnErr = transaction_util.MakeExternalWithdrawTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + solana.Signature(virtualSignatureBytes), + virtualNonce, + solana.Blockhash(virtualBlockhashBytes), + + common.CodeVmAccount, + nonceMemory, + nonceIndex, + sourceMemory, + sourceIndex, + + sourceTimelockAccounts, + destinationTokenAccount, + ) + } + if makeTxnErr != nil { + return nil, makeTxnErr } return &txn, nil } @@ -1099,15 +1152,6 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, errors.New("commitment in unexpected state") } - timelockRecord, err := h.data.GetTimelockByVault(ctx, commitmentRecord.Destination) - if err != nil { - return nil, err - } - destinationTimelockOwner, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) - if err != nil { - return nil, err - } - treasuryPool, err := common.NewAccountFromPublicKeyString(commitmentRecord.Pool) if err != nil { return nil, err @@ -1143,37 +1187,72 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c return nil, err } - _, timelockAccountMemory, timelockAccountIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationTimelockOwner) + relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, common.CodeVmAccount, cvm.VirtualAccountTypeRelay, commitmentVault) if err != nil { return nil, err } - relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, common.CodeVmAccount, cvm.VirtualAccountTypeRelay, commitmentVault) + isInternal, err := isInternalVmTransfer(ctx, h.data, destination) if err != nil { return nil, err } - // todo: support external transfers - txn, err := transaction_util.MakeInternalTreasuryAdvanceTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + var txn solana.Transaction + var makeTxnErr error + if isInternal { + destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, commitmentRecord.Destination) + if err != nil { + return nil, err + } - common.CodeVmAccount, - timelockAccountMemory, - timelockAccountIndex, - relayMemory, - relayAccountIndex, + destinationOwner, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) + if err != nil { + return nil, err + } - treasuryPool, - treasuryPoolVault, - destination, - commitment, - commitmentRecord.Amount, - transcript, - recentRoot, - ) - if err != nil { - return nil, err + _, timelockAccountMemory, timelockAccountIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationOwner) + if err != nil { + return nil, err + } + + txn, makeTxnErr = transaction_util.MakeInternalTreasuryAdvanceTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + common.CodeVmAccount, + timelockAccountMemory, + timelockAccountIndex, + relayMemory, + relayAccountIndex, + + treasuryPool, + treasuryPoolVault, + destination, + commitment, + commitmentRecord.Amount, + transcript, + recentRoot, + ) + } else { + txn, makeTxnErr = transaction_util.MakeExternalTreasuryAdvanceTransaction( + selectedNonce.Account, + selectedNonce.Blockhash, + + common.CodeVmAccount, + relayMemory, + relayAccountIndex, + + treasuryPool, + treasuryPoolVault, + destination, + commitment, + commitmentRecord.Amount, + transcript, + recentRoot, + ) + } + if makeTxnErr != nil { + return nil, makeTxnErr } return &txn, nil } diff --git a/pkg/code/async/sequencer/vm.go b/pkg/code/async/sequencer/vm.go index ca505eae..dc4dd5c4 100644 --- a/pkg/code/async/sequencer/vm.go +++ b/pkg/code/async/sequencer/vm.go @@ -12,6 +12,7 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/cvm/ram" "github.com/code-payments/code-server/pkg/code/data/cvm/storage" + "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/solana/cvm" ) @@ -169,3 +170,17 @@ func getVirtualRelayAccountStateInMemory(ctx context.Context, vmIndexerClient in return &state, memory, uint16(protoMemory.Index), nil } + +func isInternalVmTransfer(ctx context.Context, data code_data.Provider, destination *common.Account) (bool, error) { + // We only track Timelock managed within our VM, so the presence of a record + // is sufficient to determine internal/external transfer status + _, err := data.GetTimelockByVault(ctx, destination.PublicKey().ToBase58()) + switch err { + case nil: + return true, nil + case timelock.ErrTimelockNotFound: + return false, nil + default: + return false, err + } +} diff --git a/pkg/code/server/grpc/transaction/v2/intent_handler.go b/pkg/code/server/grpc/transaction/v2/intent_handler.go index 252d33a3..292678f4 100644 --- a/pkg/code/server/grpc/transaction/v2/intent_handler.go +++ b/pkg/code/server/grpc/transaction/v2/intent_handler.go @@ -2541,16 +2541,13 @@ func validateNoUpgradeActions(actions []*transactionpb.Action) error { } func validateExternalKinTokenAccountWithinIntent(ctx context.Context, data code_data.Provider, tokenAccount *common.Account) error { - /* - isValid, message, err := common.ValidateExternalKinTokenAccount(ctx, data, tokenAccount) - if err != nil { - return err - } else if !isValid { - return newIntentValidationError(message) - } - return nil - */ - return newIntentDeniedError("external transfers are not yet supported") + isValid, message, err := common.ValidateExternalKinTokenAccount(ctx, data, tokenAccount) + if err != nil { + return err + } else if !isValid { + return newIntentValidationError(message) + } + return nil } func validateExchangeDataWithinIntent(ctx context.Context, data code_data.Provider, intentId string, proto *transactionpb.ExchangeData) error { diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index cbef6ab5..ee4dc9cb 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -14,8 +14,6 @@ import ( // a larger refactor going to happen anyways when we support batching of // many virtual instructions into a single Solana transaction. -// todo: Support external variation of transfers - // MakeNoncedTransaction makes a transaction that's backed by a nonce. The returned // transaction is not signed. func MakeNoncedTransaction(nonce *common.Account, bh solana.Blockhash, instructions ...solana.Instruction) (solana.Transaction, error) { @@ -111,7 +109,7 @@ func MakeInternalWithdrawTransaction( memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) memoryCPublicKeyBytes := ed25519.PublicKey(destinationMemory.PublicKey().ToBytes()) - withdrawVirtualIxn := cvm.NewVirtualInstruction( + vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), &cvm.VirtualDurableNonce{ Address: virtualNonce.PublicKey().ToBytes(), @@ -142,10 +140,73 @@ func MakeInternalWithdrawTransaction( VmMemC: &memoryCPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ - Opcode: withdrawVirtualIxn.Opcode, + Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, MemBanks: []uint8{0, 1, 2}, - Data: withdrawVirtualIxn.Data, + Data: vixn.Data, + }, + ) + + return MakeNoncedTransaction(nonce, bh, execInstruction) +} + +func MakeExternalWithdrawTransaction( + nonce *common.Account, + bh solana.Blockhash, + + virtualSignature solana.Signature, + virtualNonce *common.Account, + virtualBlockhash solana.Blockhash, + + vm *common.Account, + nonceMemory *common.Account, + nonceIndex uint16, + sourceMemory *common.Account, + sourceIndex uint16, + + source *common.TimelockAccounts, + destination *common.Account, +) (solana.Transaction, error) { + memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) + memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) + + destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) + + vixn := cvm.NewVirtualInstruction( + common.GetSubsidizer().PublicKey().ToBytes(), + &cvm.VirtualDurableNonce{ + Address: virtualNonce.PublicKey().ToBytes(), + Nonce: cvm.Hash(virtualBlockhash), + }, + cvm.NewTimelockWithdrawExternalVirtualInstructionCtor( + &cvm.TimelockWithdrawExternalVirtualInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + VirtualTimelock: source.State.PublicKey().ToBytes(), + VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), + Owner: source.VaultOwner.PublicKey().ToBytes(), + Destination: destination.PublicKey().ToBytes(), + Mint: source.Mint.PublicKey().ToBytes(), + }, + &cvm.TimelockWithdrawExternalVirtualInstructionArgs{ + TimelockBump: source.StateBump, + Signature: cvm.Signature(virtualSignature), + }, + ), + ) + + execInstruction := cvm.NewVmExecInstruction( + &cvm.VmExecInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmMemA: &memoryAPublicKeyBytes, + VmMemB: &memoryBPublicKeyBytes, + ExternalAddress: &destinationPublicKeyBytes, + }, + &cvm.VmExecInstructionArgs{ + Opcode: vixn.Opcode, + MemIndices: []uint16{nonceIndex, sourceIndex}, + MemBanks: []uint8{0, 1}, + Data: vixn.Data, }, ) @@ -176,7 +237,7 @@ func MakeInternalTransferWithAuthorityTransaction( memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) memoryCPublicKeyBytes := ed25519.PublicKey(destinationMemory.PublicKey().ToBytes()) - transferVirtualIxn := cvm.NewVirtualInstruction( + vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), &cvm.VirtualDurableNonce{ Address: virtualNonce.PublicKey().ToBytes(), @@ -207,10 +268,74 @@ func MakeInternalTransferWithAuthorityTransaction( VmMemC: &memoryCPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ - Opcode: transferVirtualIxn.Opcode, + Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, MemBanks: []uint8{0, 1, 2}, - Data: transferVirtualIxn.Data, + Data: vixn.Data, + }, + ) + + return MakeNoncedTransaction(nonce, bh, execInstruction) +} + +func MakeExternalTransferWithAuthorityTransaction( + nonce *common.Account, + bh solana.Blockhash, + + virtualSignature solana.Signature, + virtualNonce *common.Account, + virtualBlockhash solana.Blockhash, + + vm *common.Account, + nonceMemory *common.Account, + nonceIndex uint16, + sourceMemory *common.Account, + sourceIndex uint16, + + source *common.TimelockAccounts, + destination *common.Account, + kinAmountInQuarks uint64, +) (solana.Transaction, error) { + memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) + memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) + + destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) + + vixn := cvm.NewVirtualInstruction( + common.GetSubsidizer().PublicKey().ToBytes(), + &cvm.VirtualDurableNonce{ + Address: virtualNonce.PublicKey().ToBytes(), + Nonce: cvm.Hash(virtualBlockhash), + }, + cvm.NewTimelockTransferInternalVirtualInstructionCtor( + &cvm.TimelockTransferInternalVirtualInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + VirtualTimelock: source.State.PublicKey().ToBytes(), + VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), + Owner: source.VaultOwner.PublicKey().ToBytes(), + Destination: destination.PublicKey().ToBytes(), + }, + &cvm.TimelockTransferInternalVirtualInstructionArgs{ + TimelockBump: source.StateBump, + Amount: kinAmountInQuarks, + Signature: cvm.Signature(virtualSignature), + }, + ), + ) + + execInstruction := cvm.NewVmExecInstruction( + &cvm.VmExecInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmMemA: &memoryAPublicKeyBytes, + VmMemB: &memoryBPublicKeyBytes, + ExternalAddress: &destinationPublicKeyBytes, + }, + &cvm.VmExecInstructionArgs{ + Opcode: vixn.Opcode, + MemIndices: []uint16{nonceIndex, sourceIndex}, + MemBanks: []uint8{0, 1}, + Data: vixn.Data, }, ) @@ -241,7 +366,7 @@ func MakeInternalTreasuryAdvanceTransaction( treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) - relayTransferInternalVirtualInstruction := cvm.NewVirtualInstruction( + vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), nil, cvm.NewRelayTransferInternalVirtualInstructionCtor( @@ -265,10 +390,67 @@ func MakeInternalTreasuryAdvanceTransaction( VmRelayVault: &treasuryPoolVaultPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ - Opcode: relayTransferInternalVirtualInstruction.Opcode, + Opcode: vixn.Opcode, MemIndices: []uint16{accountIndex, relayIndex}, MemBanks: []uint8{0, 1}, - Data: relayTransferInternalVirtualInstruction.Data, + Data: vixn.Data, + }, + ) + + return MakeNoncedTransaction(nonce, bh, execInstruction) +} + +func MakeExternalTreasuryAdvanceTransaction( + nonce *common.Account, + bh solana.Blockhash, + + vm *common.Account, + relayMemory *common.Account, + relayIndex uint16, + + treasuryPool *common.Account, + treasuryPoolVault *common.Account, + destination *common.Account, + commitment *common.Account, + kinAmountInQuarks uint64, + transcript []byte, + recentRoot []byte, +) (solana.Transaction, error) { + memoryAPublicKeyBytes := ed25519.PublicKey(relayMemory.PublicKey().ToBytes()) + + treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) + treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) + + destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) + + vixn := cvm.NewVirtualInstruction( + common.GetSubsidizer().PublicKey().ToBytes(), + nil, + cvm.NewRelayTransferExternalVirtualInstructionCtor( + &cvm.RelayTransferExternalVirtualInstructionAccounts{}, + &cvm.RelayTransferExternalVirtualInstructionArgs{ + Transcript: cvm.Hash(transcript), + RecentRoot: cvm.Hash(recentRoot), + Commitment: cvm.Hash(commitment.PublicKey().ToBytes()), + Amount: kinAmountInQuarks, + }, + ), + ) + + execInstruction := cvm.NewVmExecInstruction( + &cvm.VmExecInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: vm.PublicKey().ToBytes(), + VmMemA: &memoryAPublicKeyBytes, + VmRelay: &treasuryPoolPublicKeyBytes, + VmRelayVault: &treasuryPoolVaultPublicKeyBytes, + ExternalAddress: &destinationPublicKeyBytes, + }, + &cvm.VmExecInstructionArgs{ + Opcode: vixn.Opcode, + MemIndices: []uint16{relayIndex}, + MemBanks: []uint8{0}, + Data: vixn.Data, }, ) @@ -308,7 +490,7 @@ func MakeCashChequeTransaction( treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) - transferRelayVirtualIxn := cvm.NewVirtualInstruction( + vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), &cvm.VirtualDurableNonce{ Address: virtualNonce.PublicKey().ToBytes(), @@ -343,10 +525,10 @@ func MakeCashChequeTransaction( ExternalAddress: &treasuryPoolVaultPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ - Opcode: transferRelayVirtualIxn.Opcode, + Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, relayIndex}, MemBanks: []uint8{0, 1, 2}, - Data: transferRelayVirtualIxn.Data, + Data: vixn.Data, }, ) diff --git a/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go b/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go index 8a576727..8362ac9a 100644 --- a/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go +++ b/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go @@ -11,12 +11,12 @@ const ( TimelockWithdrawEnternalVirtrualInstructionDataSize = SignatureSize // signature ) -type TimelockWithdrawEnternalVirtualInstructionArgs struct { +type TimelockWithdrawExternalVirtualInstructionArgs struct { TimelockBump uint8 Signature Signature } -type TimelockWithdrawEnternalVirtualInstructionAccounts struct { +type TimelockWithdrawExternalVirtualInstructionAccounts struct { VmAuthority ed25519.PublicKey VirtualTimelock ed25519.PublicKey VirtualTimelockVault ed25519.PublicKey @@ -25,9 +25,9 @@ type TimelockWithdrawEnternalVirtualInstructionAccounts struct { Mint ed25519.PublicKey } -func NewTimelockWithdrawEnternalVirtualInstructionCtor( - accounts *TimelockWithdrawEnternalVirtualInstructionAccounts, - args *TimelockWithdrawEnternalVirtualInstructionArgs, +func NewTimelockWithdrawExternalVirtualInstructionCtor( + accounts *TimelockWithdrawExternalVirtualInstructionAccounts, + args *TimelockWithdrawExternalVirtualInstructionArgs, ) VirtualInstructionCtor { return func() (Opcode, []solana.Instruction, []byte) { var offset int From 97a0c216a00f5ca59cd4b253e12640505df1308b Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 12 Sep 2024 10:43:35 -0700 Subject: [PATCH 35/79] Support privacy upgrades for the VM (#180) * Reintroduce and fix privacy upgrade intent and action handlers in SubmitIntet * Reintroduce and fix privacy upgrade RPCs * Use virtual nonce when extracting from fulfillment to upgrade * Update TestNonce_SelectAvailableNonce --- .../grpc/transaction/v2/action_handler.go | 52 ++++------ pkg/code/server/grpc/transaction/v2/intent.go | 98 +++++++++++-------- .../grpc/transaction/v2/intent_handler.go | 2 - pkg/code/server/grpc/transaction/v2/proof.go | 21 +++- pkg/code/transaction/nonce.go | 30 +++--- pkg/code/transaction/nonce_test.go | 45 ++++----- pkg/code/transaction/virtual_instruction.go | 74 ++++++++++---- 7 files changed, 191 insertions(+), 131 deletions(-) diff --git a/pkg/code/server/grpc/transaction/v2/action_handler.go b/pkg/code/server/grpc/transaction/v2/action_handler.go index 09908d8e..2f366250 100644 --- a/pkg/code/server/grpc/transaction/v2/action_handler.go +++ b/pkg/code/server/grpc/transaction/v2/action_handler.go @@ -714,7 +714,7 @@ func (h *TemporaryPrivacyTransferActionHandler) GetFulfillmentMetadata( fulfillmentType: fulfillment.TemporaryPrivacyTransferWithAuthority, source: h.source.Vault, - destination: h.commitmentVault, + destination: h.commitmentVault, // Technically treasury vault with VM, but would break a number of things that we don't want to deal with for now fulfillmentOrderingIndex: 2000, disableActiveScheduling: true, }, nil @@ -727,7 +727,6 @@ func (h *TemporaryPrivacyTransferActionHandler) OnSaveToDB(ctx context.Context) return h.data.SaveCommitment(ctx, h.unsavedCommitmentRecord) } -/* // Handles both of the equivalent client transfer and exchange actions. The // server-defined action only defines the private movement of funds between // accounts and it's all treated the same by backend processes. The client @@ -736,10 +735,9 @@ func (h *TemporaryPrivacyTransferActionHandler) OnSaveToDB(ctx context.Context) type PermanentPrivacyUpgradeActionHandler struct { data code_data.Provider - source *common.TimelockAccounts - oldCommitmentVault *common.Account - privacyUpgradeProof *privacyUpgradeProof - amount uint64 + source *common.TimelockAccounts + commitmentBeingUpgraded *commitment.Record + privacyUpgradeProof *privacyUpgradeProof fulfillmentToUpgrade *fulfillment.Record } @@ -761,39 +759,36 @@ func NewPermanentPrivacyUpgradeActionHandler( return nil, err } - var txnToUpgrade solana.Transaction - err = txnToUpgrade.Unmarshal(h.fulfillmentToUpgrade.Data) + h.commitmentBeingUpgraded, err = h.data.GetCommitmentByAction(ctx, intentRecord.IntentId, protoAction.ActionId) if err != nil { return nil, err } - oldIxnArgs, oldIxnAccounts, err := timelock_token_v1.TransferWithAuthorityInstructionFromLegacyInstruction(txnToUpgrade, 2) + h.privacyUpgradeProof, err = getProofForPrivacyUpgrade(ctx, h.data, cachedUpgradeTarget) if err != nil { return nil, err } - authority, err := common.NewAccountFromPublicKeyBytes(oldIxnAccounts.VaultOwner) + actionRecord, err := h.data.GetActionById(ctx, intentRecord.IntentId, protoAction.ActionId) if err != nil { return nil, err } - h.source, err = authority.GetTimelockAccounts(timelock_token_v1.DataVersion1, common.KinMintAccount) + sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, actionRecord.Source) if err != nil { return nil, err } - h.oldCommitmentVault, err = common.NewAccountFromPublicKeyBytes(oldIxnAccounts.Destination) + authority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) if err != nil { return nil, err } - h.privacyUpgradeProof, err = getProofForPrivacyUpgrade(ctx, h.data, cachedUpgradeTarget) + h.source, err = authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) if err != nil { return nil, err } - h.amount = oldIxnArgs.Amount - return h, nil } @@ -840,43 +835,38 @@ func (h *PermanentPrivacyUpgradeActionHandler) getFulfillmentBeingUpgraded(ctx c return fulfillmentRecords[0], nil } -func (h *PermanentPrivacyUpgradeActionHandler) MakeUpgradedSolanaTransaction( +func (h *PermanentPrivacyUpgradeActionHandler) GetFulfillmentMetadata( nonce *common.Account, bh solana.Blockhash, -) (*makeSolanaTransactionResult, error) { - txn, err := transaction_util.MakeTransferWithAuthorityTransaction( +) (*newFulfillmentMetadata, error) { + virtualIxnHash, err := transaction_util.GetVirtualTransferWithAuthorityHash( nonce, bh, h.source, h.privacyUpgradeProof.newCommitmentVault, - h.amount, + h.commitmentBeingUpgraded.Amount, ) if err != nil { return nil, err } - return &makeSolanaTransactionResult{ - txn: &txn, + return &newFulfillmentMetadata{ + requiresClientSignature: true, + expectedSigner: h.source.VaultOwner, + virtualIxnHash: virtualIxnHash, fulfillmentType: fulfillment.PermanentPrivacyTransferWithAuthority, source: h.source.Vault, - destination: h.privacyUpgradeProof.newCommitmentVault, + destination: h.privacyUpgradeProof.newCommitmentVault, // Technically treasury vault with VM, but would break a number of things that we don't want to deal with for now fulfillmentOrderingIndex: 1000, }, nil } func (h *PermanentPrivacyUpgradeActionHandler) OnSaveToDB(ctx context.Context) error { - commitmentBeingUpgraded, err := h.data.GetCommitmentByVault(ctx, h.oldCommitmentVault.PublicKey().ToBase58()) - if err != nil { - return err - } - newDestination := h.privacyUpgradeProof.newCommitmentVault.PublicKey().ToBase58() - commitmentBeingUpgraded.RepaymentDivertedTo = &newDestination - - return h.data.SaveCommitment(ctx, commitmentBeingUpgraded) + h.commitmentBeingUpgraded.RepaymentDivertedTo = &newDestination + return h.data.SaveCommitment(ctx, h.commitmentBeingUpgraded) } -*/ func getTransript( intent string, diff --git a/pkg/code/server/grpc/transaction/v2/intent.go b/pkg/code/server/grpc/transaction/v2/intent.go index 77e100fd..e05e307d 100644 --- a/pkg/code/server/grpc/transaction/v2/intent.go +++ b/pkg/code/server/grpc/transaction/v2/intent.go @@ -6,6 +6,7 @@ import ( "crypto/ed25519" "database/sql" "encoding/base64" + "encoding/hex" "strings" "time" @@ -23,8 +24,10 @@ import ( chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" + "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/nonce" @@ -115,11 +118,9 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm log = log.WithField("intent_type", "receive_payments_privately") intentHandler = NewReceivePaymentsPrivatelyIntentHandler(s.conf, s.data, s.antispamGuard, s.amlGuard) intentRequiresNewTreasuryPoolFunds = true - /* - case *transactionpb.Metadata_UpgradePrivacy: - log = log.WithField("intent_type", "upgrade_privacy") - intentHandler = NewUpgradePrivacyIntentHandler(s.conf, s.data) - */ + case *transactionpb.Metadata_UpgradePrivacy: + log = log.WithField("intent_type", "upgrade_privacy") + intentHandler = NewUpgradePrivacyIntentHandler(s.conf, s.data) case *transactionpb.Metadata_SendPublicPayment: log = log.WithField("intent_type", "send_public_payment") intentHandler = NewSendPublicPaymentIntentHandler(s.conf, s.data, s.pusher, s.antispamGuard, s.maxmind) @@ -447,27 +448,25 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm log = log.WithField("action_type", "temporary_privacy_exchange") actionType = action.PrivateTransfer actionHandler, err = NewTemporaryPrivacyTransferActionHandler(ctx, s.conf, s.data, intentRecord, protoAction, true, s.selectTreasuryPoolForAdvance) - /* - case *transactionpb.Action_PermanentPrivacyUpgrade: - log = log.WithField("action_type", "permanent_privacy_upgrade") - actionType = action.PrivateTransfer - - // Pass along the privacy upgrade target found during intent validation - // to avoid duplication of work. - cachedUpgradeTarget, ok := intentHandler.(*UpgradePrivacyIntentHandler).GetCachedUpgradeTarget(typed.PermanentPrivacyUpgrade) - if !ok { - log.Warn("cached privacy upgrade target not found") - return handleSubmitIntentError(streamer, errors.New("cached privacy upgrade target not found")) - } + case *transactionpb.Action_PermanentPrivacyUpgrade: + log = log.WithField("action_type", "permanent_privacy_upgrade") + actionType = action.PrivateTransfer - actionHandler, err = NewPermanentPrivacyUpgradeActionHandler( - ctx, - s.data, - intentRecord, - typed.PermanentPrivacyUpgrade, - cachedUpgradeTarget, - ) - */ + // Pass along the privacy upgrade target found during intent validation + // to avoid duplication of work. + cachedUpgradeTarget, ok := intentHandler.(*UpgradePrivacyIntentHandler).GetCachedUpgradeTarget(typed.PermanentPrivacyUpgrade) + if !ok { + log.Warn("cached privacy upgrade target not found") + return handleSubmitIntentError(streamer, errors.New("cached privacy upgrade target not found")) + } + + actionHandler, err = NewPermanentPrivacyUpgradeActionHandler( + ctx, + s.data, + intentRecord, + typed.PermanentPrivacyUpgrade, + cachedUpgradeTarget, + ) default: return handleSubmitIntentError(streamer, status.Errorf(codes.InvalidArgument, "SubmitIntentRequest.SubmitActions.Actions[%d].Type is nil", i)) } @@ -541,9 +540,7 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm // Re-use the same nonce as the one in the fulfillment we're upgrading, // so we avoid server from submitting both. - // - // todo: This doesn't select the virtual durable nonce - selectedNonce, err = transaction.SelectNonceFromFulfillmentToUpgrade(ctx, s.data, fulfillmentToUpgrade) + selectedNonce, err = transaction.SelectVirtualNonceFromFulfillmentToUpgrade(ctx, s.data, fulfillmentToUpgrade) if err != nil { log.WithError(err).Warn("failure selecting nonce from existing fulfillment") return handleSubmitIntentError(streamer, err) @@ -1257,7 +1254,6 @@ func (s *transactionServer) CanWithdrawToAccount(ctx context.Context, req *trans }, nil } -/* func (s *transactionServer) GetPrivacyUpgradeStatus(ctx context.Context, req *transactionpb.GetPrivacyUpgradeStatusRequest) (*transactionpb.GetPrivacyUpgradeStatusResponse, error) { intentId := base58.Encode(req.IntentId.Value) @@ -1412,23 +1408,19 @@ func toUpgradeableIntentProto(ctx context.Context, data code_data.Provider, inte return nil, err } - if len(fulfillmentRecords) != 1 || *fulfillmentRecords[0].Destination != commitmentRecord.Vault { + if len(fulfillmentRecords) != 1 || *fulfillmentRecords[0].Destination != commitmentRecord.VaultAddress { return nil, errors.New("fulfillment to upgrade was not found") } fulfillmentToUpgrade := fulfillmentRecords[0] - var txn solana.Transaction - err = txn.Unmarshal(fulfillmentToUpgrade.Data) + nonce, err := common.NewAccountFromPublicKeyString(*fulfillmentToUpgrade.VirtualNonce) if err != nil { return nil, err } - clientSignature := txn.Signatures[clientSignatureIndex] - - // Clear out all signatures, so clients have no way of submitting this transaction - var emptySig solana.Signature - for i := range txn.Signatures { - copy(txn.Signatures[i][:], emptySig[:]) + bh, err := base58.Decode(*fulfillmentToUpgrade.VirtualBlockhash) + if err != nil { + return nil, err } // todo: this can be heavily cached @@ -1437,6 +1429,16 @@ func toUpgradeableIntentProto(ctx context.Context, data code_data.Provider, inte return nil, err } + sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfo.AuthorityAccount) + if err != nil { + return nil, err + } + + sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + return nil, err + } + originalDestination, err := common.NewAccountFromPublicKeyString(commitmentRecord.Destination) if err != nil { return nil, err @@ -1452,12 +1454,29 @@ func toUpgradeableIntentProto(ctx context.Context, data code_data.Provider, inte return nil, err } + txn, err := transaction.GetVirtualTransferWithAuthorityTransaction( + nonce, + solana.Blockhash(bh), + + sourceTimelockAccounts, + originalDestination, + commitmentRecord.Amount, + ) + if err != nil { + return nil, err + } + + clientSignature, err := base58.Decode(*fulfillmentToUpgrade.VirtualSignature) + if err != nil { + return nil, err + } + action := &transactionpb.UpgradeableIntent_UpgradeablePrivateAction{ TransactionBlob: &commonpb.Transaction{ Value: txn.Marshal(), }, ClientSignature: &commonpb.Signature{ - Value: clientSignature[:], + Value: clientSignature, }, ActionId: commitmentRecord.ActionId, SourceAccountType: sourceAccountInfo.AccountType, @@ -1479,4 +1498,3 @@ func toUpgradeableIntentProto(ctx context.Context, data code_data.Provider, inte Actions: actions, }, nil } -*/ diff --git a/pkg/code/server/grpc/transaction/v2/intent_handler.go b/pkg/code/server/grpc/transaction/v2/intent_handler.go index 292678f4..f2bb112d 100644 --- a/pkg/code/server/grpc/transaction/v2/intent_handler.go +++ b/pkg/code/server/grpc/transaction/v2/intent_handler.go @@ -1300,7 +1300,6 @@ func (h *ReceivePaymentsPrivatelyIntentHandler) OnCommittedToDB(ctx context.Cont return nil } -/* type UpgradePrivacyIntentHandler struct { conf *conf data code_data.Provider @@ -1354,7 +1353,6 @@ func (h *UpgradePrivacyIntentHandler) GetCachedUpgradeTarget(protoAction *transa upgradeTo, ok := h.cachedUpgradeTargets[protoAction.ActionId] return upgradeTo, ok } -*/ type SendPublicPaymentIntentHandler struct { conf *conf diff --git a/pkg/code/server/grpc/transaction/v2/proof.go b/pkg/code/server/grpc/transaction/v2/proof.go index cee818df..46195d76 100644 --- a/pkg/code/server/grpc/transaction/v2/proof.go +++ b/pkg/code/server/grpc/transaction/v2/proof.go @@ -1,6 +1,22 @@ package transaction_v2 -/* +import ( + "context" + "encoding/hex" + "errors" + "sync" + "time" + + "github.com/mr-tron/base58" + + commitment_worker "github.com/code-payments/code-server/pkg/code/async/commitment" + "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/code/data/action" + "github.com/code-payments/code-server/pkg/code/data/commitment" + "github.com/code-payments/code-server/pkg/code/data/merkletree" +) + type refreshingMerkleTree struct { tree *merkletree.MerkleTree lastRefreshedAt time.Time @@ -186,7 +202,7 @@ func getProofForPrivacyUpgrade(ctx context.Context, data code_data.Provider, upg return nil, err } - newCommitmentVaultAccount, err := common.NewAccountFromPublicKeyString(upgradingTo.newCommitmentRecord.Vault) + newCommitmentVaultAccount, err := common.NewAccountFromPublicKeyString(upgradingTo.newCommitmentRecord.VaultAddress) if err != nil { return nil, err } @@ -253,4 +269,3 @@ func getCachedMerkleTreeForTreasury(ctx context.Context, data code_data.Provider return cached.tree, nil } -*/ diff --git a/pkg/code/transaction/nonce.go b/pkg/code/transaction/nonce.go index d5642432..7a1bfa86 100644 --- a/pkg/code/transaction/nonce.go +++ b/pkg/code/transaction/nonce.go @@ -126,22 +126,22 @@ func SelectAvailableNonce(ctx context.Context, data code_data.Provider, env nonc }, nil } -// SelectNonceFromFulfillmentToUpgrade selects a nonce from a fulfillment that +// SelectVirtualNonceFromFulfillmentToUpgrade selects a nonce from a fulfillment that // is going to be upgraded. -func SelectNonceFromFulfillmentToUpgrade(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) (*SelectedNonce, error) { - if fulfillmentRecord.State != fulfillment.StateUnknown { +func SelectVirtualNonceFromFulfillmentToUpgrade(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record) (*SelectedNonce, error) { + if fulfillmentRecord.State != fulfillment.StateUnknown || fulfillmentRecord.Signature != nil { return nil, errors.New("dangerous nonce selection from fulfillment") } - if fulfillmentRecord.Nonce == nil { - return nil, errors.New("fulfillment doesn't have an assigned nonce") + if fulfillmentRecord.VirtualNonce == nil { + return nil, errors.New("fulfillment doesn't have an assigned virtual nonce") } - lock := getNonceLock(*fulfillmentRecord.Nonce) + lock := getNonceLock(*fulfillmentRecord.VirtualNonce) lock.Lock() // Fetch after locking to get most up-to-date state - nonceRecord, err := data.GetNonce(ctx, *fulfillmentRecord.Nonce) + nonceRecord, err := data.GetNonce(ctx, *fulfillmentRecord.VirtualNonce) if err != nil { lock.Unlock() return nil, err @@ -149,17 +149,17 @@ func SelectNonceFromFulfillmentToUpgrade(ctx context.Context, data code_data.Pro if nonceRecord.State != nonce.StateReserved { lock.Unlock() - return nil, errors.New("nonce isn't reserved") + return nil, errors.New("virtual nonce isn't reserved") } - if nonceRecord.Blockhash != *fulfillmentRecord.Blockhash { + if nonceRecord.Blockhash != *fulfillmentRecord.VirtualBlockhash { lock.Unlock() - return nil, errors.New("fulfillment record doesn't have the right blockhash") + return nil, errors.New("fulfillment record doesn't have the right virtual blockhash") } - if nonceRecord.Signature != *fulfillmentRecord.Signature { + if nonceRecord.Signature != *fulfillmentRecord.VirtualSignature { lock.Unlock() - return nil, errors.New("nonce isn't mapped to selected fulfillment") + return nil, errors.New("virtual nonce isn't mapped to selected fulfillment") } account, err := common.NewAccountFromPublicKeyString(nonceRecord.Address) @@ -168,20 +168,18 @@ func SelectNonceFromFulfillmentToUpgrade(ctx context.Context, data code_data.Pro return nil, err } - var bh solana.Blockhash - untypedBlockhash, err := base58.Decode(*fulfillmentRecord.Blockhash) + bh, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) if err != nil { lock.Unlock() return nil, err } - copy(bh[:], untypedBlockhash) return &SelectedNonce{ distributedLock: lock, data: data, record: nonceRecord, Account: account, - Blockhash: bh, + Blockhash: solana.Blockhash(bh), }, nil } diff --git a/pkg/code/transaction/nonce_test.go b/pkg/code/transaction/nonce_test.go index f702e8bd..8c799699 100644 --- a/pkg/code/transaction/nonce_test.go +++ b/pkg/code/transaction/nonce_test.go @@ -32,6 +32,9 @@ func TestNonce_SelectAvailableNonce(t *testing.T) { _, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) assert.Equal(t, ErrNoAvailableNonces, err) + _, err = SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentCvm, testutil.NewRandomAccount(t).PublicKey().ToBase58(), nonce.PurposeClientTransaction) + assert.Equal(t, ErrNoAvailableNonces, err) + selectedNonces := make(map[string]struct{}) for i := 0; i < len(noncesByAddress); i++ { selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) @@ -61,33 +64,31 @@ func TestNonce_SelectAvailableNonce(t *testing.T) { _, err = SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) assert.Equal(t, ErrNoAvailableNonces, err) - _, err = SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentCvm, testutil.NewRandomAccount(t).PublicKey().ToBase58(), nonce.PurposeClientTransaction) - assert.Equal(t, ErrNoAvailableNonces, err) } func TestNonce_SelectNonceFromFulfillmentToUpgrade_HappyPath(t *testing.T) { env := setupNonceTestEnv(t) - generateAvailableNonces(t, env, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction, 2) + generateAvailableNonces(t, env, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), nonce.PurposeClientTransaction, 2) - selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) + selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), nonce.PurposeClientTransaction) require.NoError(t, err) fulfillmentToUpgrade := &fulfillment.Record{ - Nonce: pointer.String(selectedNonce.Account.PublicKey().ToBase58()), - Blockhash: pointer.String(base58.Encode(selectedNonce.Blockhash[:])), - Signature: pointer.String("signature"), + VirtualNonce: pointer.String(selectedNonce.Account.PublicKey().ToBase58()), + VirtualBlockhash: pointer.String(base58.Encode(selectedNonce.Blockhash[:])), + VirtualSignature: pointer.String("signature"), } - require.NoError(t, selectedNonce.MarkReservedWithSignature(env.ctx, *fulfillmentToUpgrade.Signature)) + require.NoError(t, selectedNonce.MarkReservedWithSignature(env.ctx, *fulfillmentToUpgrade.VirtualSignature)) selectedNonce.Unlock() - selectedNonce, err = SelectNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) + selectedNonce, err = SelectVirtualNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) require.NoError(t, err) - assert.Equal(t, *fulfillmentToUpgrade.Nonce, selectedNonce.Account.PublicKey().ToBase58()) - assert.Equal(t, *fulfillmentToUpgrade.Blockhash, base58.Encode(selectedNonce.Blockhash[:])) + assert.Equal(t, *fulfillmentToUpgrade.VirtualNonce, selectedNonce.Account.PublicKey().ToBase58()) + assert.Equal(t, *fulfillmentToUpgrade.VirtualBlockhash, base58.Encode(selectedNonce.Blockhash[:])) require.NoError(t, selectedNonce.UpdateSignature(env.ctx, "new_signature")) @@ -98,25 +99,25 @@ func TestNonce_SelectNonceFromFulfillmentToUpgrade_HappyPath(t *testing.T) { selectedNonce.Unlock() - _, err = SelectNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) + _, err = SelectVirtualNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) assert.Error(t, err) } -func TestNonce_SelectNonceFromFulfillmentToUpgrade_DangerousPath(t *testing.T) { +func TestNonce_SelectVirtualNonceFromFulfillmentToUpgrade_DangerousPath(t *testing.T) { env := setupNonceTestEnv(t) - generateAvailableNonces(t, env, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction, 2) + generateAvailableNonces(t, env, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), nonce.PurposeClientTransaction, 2) - selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) + selectedNonce, err := SelectAvailableNonce(env.ctx, env.data, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), nonce.PurposeClientTransaction) require.NoError(t, err) fulfillmentToUpgrade := &fulfillment.Record{ - Nonce: pointer.String(selectedNonce.Account.PublicKey().ToBase58()), - Blockhash: pointer.String(base58.Encode(selectedNonce.Blockhash[:])), - Signature: pointer.String("signature"), + VirtualNonce: pointer.String(selectedNonce.Account.PublicKey().ToBase58()), + VirtualBlockhash: pointer.String(base58.Encode(selectedNonce.Blockhash[:])), + VirtualSignature: pointer.String("signature"), } - require.NoError(t, selectedNonce.MarkReservedWithSignature(env.ctx, *fulfillmentToUpgrade.Signature)) + require.NoError(t, selectedNonce.MarkReservedWithSignature(env.ctx, *fulfillmentToUpgrade.VirtualSignature)) selectedNonce.Unlock() @@ -127,21 +128,21 @@ func TestNonce_SelectNonceFromFulfillmentToUpgrade_DangerousPath(t *testing.T) { nonceRecord.Blockhash = "Cmui8pHYbKKox8g7n7xa2Qaxh1TSJsHpr3xCeNaEisdy" require.NoError(t, env.data.SaveNonce(env.ctx, nonceRecord)) - _, err = SelectNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) + _, err = SelectVirtualNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) assert.Error(t, err) nonceRecord.Blockhash = originalBlockhash nonceRecord.State = nonce.StateAvailable require.NoError(t, env.data.SaveNonce(env.ctx, nonceRecord)) - _, err = SelectNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) + _, err = SelectVirtualNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) assert.Error(t, err) nonceRecord.State = nonce.StateReserved nonceRecord.Signature = "other_signature" require.NoError(t, env.data.SaveNonce(env.ctx, nonceRecord)) - _, err = SelectNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) + _, err = SelectVirtualNonceFromFulfillmentToUpgrade(env.ctx, env.data, fulfillmentToUpgrade) assert.Error(t, err) } diff --git a/pkg/code/transaction/virtual_instruction.go b/pkg/code/transaction/virtual_instruction.go index 0cf2ebe4..e7cf2dd7 100644 --- a/pkg/code/transaction/virtual_instruction.go +++ b/pkg/code/transaction/virtual_instruction.go @@ -16,59 +16,101 @@ func GetVirtualTransferWithAuthorityHash( destination *common.Account, kinAmountInQuarks uint64, ) (*cvm.Hash, error) { - memoInstruction, err := MakeKreMemoInstruction() + txn, err := GetVirtualTransferWithAuthorityTransaction( + nonce, + bh, + source, + destination, + kinAmountInQuarks, + ) if err != nil { return nil, err } - transferWithAuthorityInstruction, err := source.GetTransferWithAuthorityInstruction(destination, kinAmountInQuarks) + hash := getVirtualTransactionHash(&txn) + return &hash, nil +} + +func GetVirtualCloseAccountWithBalanceHash( + nonce *common.Account, + bh solana.Blockhash, + + source *common.TimelockAccounts, + destination *common.Account, +) (*cvm.Hash, error) { + txn, err := GetVirtualWithdrawTransaction( + nonce, + bh, + source, + destination, + ) if err != nil { return nil, err } + hash := getVirtualTransactionHash(&txn) + return &hash, nil +} + +func GetVirtualTransferWithAuthorityTransaction( + nonce *common.Account, + bh solana.Blockhash, + + source *common.TimelockAccounts, + destination *common.Account, + kinAmountInQuarks uint64, +) (solana.Transaction, error) { + memoInstruction, err := MakeKreMemoInstruction() + if err != nil { + return solana.Transaction{}, err + } + + transferWithAuthorityInstruction, err := source.GetTransferWithAuthorityInstruction(destination, kinAmountInQuarks) + if err != nil { + return solana.Transaction{}, err + } + instructions := []solana.Instruction{ memoInstruction, transferWithAuthorityInstruction, } txn, err := MakeNoncedTransaction(nonce, bh, instructions...) if err != nil { - return nil, err + return solana.Transaction{}, err } - - hash := getVirtualTransactionHash(&txn) - return &hash, nil + return txn, nil } -func GetVirtualCloseAccountWithBalanceHash( +func GetVirtualWithdrawTransaction( nonce *common.Account, bh solana.Blockhash, source *common.TimelockAccounts, destination *common.Account, -) (*cvm.Hash, error) { +) (solana.Transaction, error) { memoInstruction, err := MakeKreMemoInstruction() if err != nil { - return nil, err + return solana.Transaction{}, err } revokeLockInstruction, err := source.GetRevokeLockWithAuthorityInstruction() if err != nil { - return nil, err + return solana.Transaction{}, err } deactivateLockInstruction, err := source.GetDeactivateInstruction() if err != nil { - return nil, err + return solana.Transaction{}, err } withdrawInstruction, err := source.GetWithdrawInstruction(destination) if err != nil { - return nil, err + return solana.Transaction{}, err } closeInstruction, err := source.GetCloseAccountsInstruction() if err != nil { - return nil, err + return solana.Transaction{}, err } instructions := []solana.Instruction{ @@ -80,11 +122,9 @@ func GetVirtualCloseAccountWithBalanceHash( } txn, err := MakeNoncedTransaction(nonce, bh, instructions...) if err != nil { - return nil, err + return solana.Transaction{}, err } - - hash := getVirtualTransactionHash(&txn) - return &hash, nil + return txn, nil } func getVirtualTransactionHash(txn *solana.Transaction) cvm.Hash { From d96e5c8196bf3518eba108535a9d40b62cd36932 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 12 Sep 2024 10:45:34 -0700 Subject: [PATCH 36/79] Disable gift card auto returns until we have a new solution (#181) --- pkg/code/async/account/service.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/code/async/account/service.go b/pkg/code/async/account/service.go index 9d8f48ca..a8ef5c6b 100644 --- a/pkg/code/async/account/service.go +++ b/pkg/code/async/account/service.go @@ -28,12 +28,15 @@ func New(data code_data.Provider, pusher push_lib.Provider, configProvider Confi } func (p *service) Start(ctx context.Context, interval time.Duration) error { - go func() { - err := p.giftCardAutoReturnWorker(ctx, interval) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("gift card auto-return processing loop terminated unexpectedly") - } - }() + // todo: auto returns are broken because we've removed close dormant account actions + /* + go func() { + err := p.giftCardAutoReturnWorker(ctx, interval) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("gift card auto-return processing loop terminated unexpectedly") + } + }() + */ go func() { err := p.swapRetryWorker(ctx, interval) From 817f8db07f1bac469a537febd237f8bc9fdcb2da Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 12 Sep 2024 13:00:21 -0700 Subject: [PATCH 37/79] Update swap destination to VM deposit token account (#182) --- go.mod | 2 +- go.sum | 4 ++-- pkg/code/server/grpc/transaction/v2/swap.go | 22 +++++++++++++++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 2ae2c35c..dcd58720 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.18.1-0.20240910221949-6bf7280b7f2b + github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886 github.com/code-payments/code-vm-indexer v0.1.0 github.com/dghubble/oauth1 v0.7.3 github.com/emirpasic/gods v1.12.0 diff --git a/go.sum b/go.sum index e6725023..9d0b0bcf 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240910221949-6bf7280b7f2b h1:FfwF4bH1QrqJ5G175PYJgbT8fXx8WiHUFUQg9auYBBY= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240910221949-6bf7280b7f2b/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886 h1:PLbMVgpFwwhI6Ji+izq/NnJQGq41OKQ2dP3oPpUEcCA= +github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/code-payments/code-vm-indexer v0.1.0 h1:XzBwFrZp1R+9POGF/zMy5o6/OCI2J+jGJ7qr4cL72rY= github.com/code-payments/code-vm-indexer v0.1.0/go.mod h1:LtXqlb7ub0mPUNKlCPJbsEDQrkZvWTPSRM5hTdHcqpM= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= diff --git a/pkg/code/server/grpc/transaction/v2/swap.go b/pkg/code/server/grpc/transaction/v2/swap.go index 841f1661..ee4214af 100644 --- a/pkg/code/server/grpc/transaction/v2/swap.go +++ b/pkg/code/server/grpc/transaction/v2/swap.go @@ -32,7 +32,9 @@ import ( "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/solana" compute_budget "github.com/code-payments/code-server/pkg/solana/computebudget" + "github.com/code-payments/code-server/pkg/solana/cvm" swap_validator "github.com/code-payments/code-server/pkg/solana/swapvalidator" + "github.com/code-payments/code-server/pkg/solana/token" "github.com/code-payments/code-server/pkg/usdc" ) @@ -138,9 +140,25 @@ func (s *transactionServer) Swap(streamer transactionpb.Transaction_SwapServer) return handleSwapError(streamer, err) } - swapDestination, err := common.NewAccountFromPublicKeyString(accountInfoRecord.TokenAccount) + vmDepositPda, _, err := cvm.GetVmDepositAddress(&cvm.GetVmDepositAddressArgs{ + Depositor: owner.PublicKey().ToBytes(), + Vm: common.CodeVmAccount.PublicKey().ToBytes(), + }) + if err != nil { + log.WithError(err).Warn("failure deriving vm deposit pda") + return handleSwapError(streamer, err) + } + + // todo: This might come from a DB record at some point + vmDepositTokenAddress, err := token.GetAssociatedAccount(vmDepositPda, kin.TokenMint) + if err != nil { + log.WithError(err).Warn("failure deriving vm deposit token account") + return handleSwapError(streamer, err) + } + + swapDestination, err := common.NewAccountFromPublicKeyBytes(vmDepositTokenAddress) if err != nil { - log.WithError(err).Warn("invalid kin primary account") + log.WithError(err).Warn("invalid vm deposit token account") return handleSwapError(streamer, err) } log = log.WithField("swap_destination", swapDestination.PublicKey().ToBase58()) From 418048b1e93562d45299b77fc0fb9bab178b6f3e Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Fri, 13 Sep 2024 11:35:34 -0700 Subject: [PATCH 38/79] Add additional VM program accounts and ixns (#183) --- pkg/solana/cvm/accounts_code_vm.go | 71 +++++++++++++ pkg/solana/cvm/accounts_relay.go | 2 +- .../instructions_system_account_decompress.go | 100 ++++++++++++++++++ pkg/solana/cvm/transaction.go | 2 - pkg/solana/cvm/types_hash.go | 9 ++ 5 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 pkg/solana/cvm/accounts_code_vm.go create mode 100644 pkg/solana/cvm/instructions_system_account_decompress.go diff --git a/pkg/solana/cvm/accounts_code_vm.go b/pkg/solana/cvm/accounts_code_vm.go new file mode 100644 index 00000000..144db502 --- /dev/null +++ b/pkg/solana/cvm/accounts_code_vm.go @@ -0,0 +1,71 @@ +package cvm + +import ( + "bytes" + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const ( + CodeVmAccountSize = (8 + //discriminator + 32 + // authority + 32 + // mint + TokenPoolSize + // omnibus + 1 + // lock_duration + 1 + // bump + 8 + // slot + HashSize + // poh + 5) // padding +) + +var CodeVmAccountDiscriminator = []byte{0xed, 0x82, 0x60, 0x0b, 0xbb, 0x2c, 0xc7, 0x55} + +type CodeVmAccount struct { + Authority ed25519.PublicKey + Mint ed25519.PublicKey + Omnibus TokenPool + LockDuration uint8 + Bump uint8 + Slot uint64 + Poh Hash + // todo: change log +} + +func (obj *CodeVmAccount) Unmarshal(data []byte) error { + if len(data) < CodeVmAccountSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, CodeVmAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.Authority, &offset) + getKey(data, &obj.Mint, &offset) + getTokenPool(data, &obj.Omnibus, &offset) + getUint8(data, &obj.LockDuration, &offset) + getUint8(data, &obj.Bump, &offset) + getUint64(data, &obj.Slot, &offset) + getHash(data, &obj.Poh, &offset) + + return nil +} + +func (obj *CodeVmAccount) String() string { + return fmt.Sprintf( + "CodeVmAccount{authority=%s,mint=%s,omnibus=%s,lock_duration=%d,bump=%d,slot=%d,poh=%s}", + base58.Encode(obj.Authority), + base58.Encode(obj.Mint), + obj.Omnibus.String(), + obj.LockDuration, + obj.Bump, + obj.Slot, + obj.Poh.String(), + ) +} diff --git a/pkg/solana/cvm/accounts_relay.go b/pkg/solana/cvm/accounts_relay.go index b87a6ce4..905573cc 100644 --- a/pkg/solana/cvm/accounts_relay.go +++ b/pkg/solana/cvm/accounts_relay.go @@ -19,7 +19,7 @@ const ( MaxRelayAccountNameSize + // name 1 + // num_levels 1 + // num_history - TokenPoolSize) // token_pool + TokenPoolSize) // treasury ) var RelayAccountDiscriminator = []byte{0xf2, 0xbb, 0xef, 0x5f, 0x89, 0xe1, 0xf5, 0x5c} diff --git a/pkg/solana/cvm/instructions_system_account_decompress.go b/pkg/solana/cvm/instructions_system_account_decompress.go new file mode 100644 index 00000000..c22c3dee --- /dev/null +++ b/pkg/solana/cvm/instructions_system_account_decompress.go @@ -0,0 +1,100 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +var SystemAccountDecompressInstructionDiscriminator = []byte{ + 0x63, 0x05, 0x07, 0x80, 0x20, 0x84, 0x86, 0x53, +} + +const ( + MinSystemAccountDecompressInstructionArgsSize = (8 + // discriminator + 2 + // account_index + 4 + // len(packed_va) + 4 + // len(proof) + SignatureSize) // signature +) + +type SystemAccountDecompressInstructionArgs struct { + AccountIndex uint16 + PackedVa []uint8 + Proof HashArray + Signature Signature +} + +type SystemAccountDecompressInstructionAccounts struct { + VmAuthority ed25519.PublicKey + Vm ed25519.PublicKey + VmMemory ed25519.PublicKey + VmStorage ed25519.PublicKey + UnlockPda *ed25519.PublicKey + WithdrawReceipt *ed25519.PublicKey +} + +func NewSystemAccountDecompressInstruction( + accounts *SystemAccountDecompressInstructionAccounts, + args *SystemAccountDecompressInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, + len(SystemAccountDecompressInstructionDiscriminator)+ + GetSystemAccountDecompressInstructionArgsSize(args)) + + putDiscriminator(data, SystemAccountDecompressInstructionDiscriminator, &offset) + putUint16(data, args.AccountIndex, &offset) + putUint8Array(data, args.PackedVa, &offset) + putHashArray(data, args.Proof, &offset) + putSignature(data, args.Signature, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.VmAuthority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmMemory, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.VmStorage, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.UnlockPda), + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: getOptionalAccountMetaAddress(accounts.WithdrawReceipt), + IsWritable: false, + IsSigner: false, + }, + }, + } +} + +func GetSystemAccountDecompressInstructionArgsSize(args *SystemAccountDecompressInstructionArgs) int { + return (MinSystemAccountDecompressInstructionArgsSize + + len(args.PackedVa) + // packed_va + HashSize*len(args.Proof)) // proof +} diff --git a/pkg/solana/cvm/transaction.go b/pkg/solana/cvm/transaction.go index c450af31..c031ab8f 100644 --- a/pkg/solana/cvm/transaction.go +++ b/pkg/solana/cvm/transaction.go @@ -4,8 +4,6 @@ import ( "crypto/ed25519" ) -// todo: Utilities for crafting a transaction with virtual instructions, but this may be too low-level of a package for that - func getOptionalAccountMetaAddress(account *ed25519.PublicKey) ed25519.PublicKey { if account != nil { return *account diff --git a/pkg/solana/cvm/types_hash.go b/pkg/solana/cvm/types_hash.go index 33e94ccf..15ac563b 100644 --- a/pkg/solana/cvm/types_hash.go +++ b/pkg/solana/cvm/types_hash.go @@ -35,6 +35,15 @@ func getHashArray(src []byte, dst *HashArray, offset *int) { getHash(src, &(*dst)[i], offset) } } +func putHashArray(dst []byte, v HashArray, offset *int) { + binary.LittleEndian.PutUint32(dst[*offset:], uint32(len(v))) + *offset += 4 + + for _, hash := range v { + copy(dst[*offset:], hash[:]) + *offset += HashSize + } +} func (h HashArray) String() string { stringValues := make([]string, len(h)) From 67f242b0ff854327b6223817ae6a002a197cc730 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 16 Sep 2024 06:58:39 -0700 Subject: [PATCH 39/79] Support change log in Code VM account (#184) --- pkg/solana/cvm/accounts_code_vm.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/solana/cvm/accounts_code_vm.go b/pkg/solana/cvm/accounts_code_vm.go index 144db502..76e98e82 100644 --- a/pkg/solana/cvm/accounts_code_vm.go +++ b/pkg/solana/cvm/accounts_code_vm.go @@ -17,7 +17,9 @@ const ( 1 + // bump 8 + // slot HashSize + // poh - 5) // padding + 5 + // padding + PagedMemorySize + // change_log + 2) // padding ) var CodeVmAccountDiscriminator = []byte{0xed, 0x82, 0x60, 0x0b, 0xbb, 0x2c, 0xc7, 0x55} @@ -30,7 +32,7 @@ type CodeVmAccount struct { Bump uint8 Slot uint64 Poh Hash - // todo: change log + ChangeLog PagedMemory } func (obj *CodeVmAccount) Unmarshal(data []byte) error { @@ -53,13 +55,15 @@ func (obj *CodeVmAccount) Unmarshal(data []byte) error { getUint8(data, &obj.Bump, &offset) getUint64(data, &obj.Slot, &offset) getHash(data, &obj.Poh, &offset) + offset += 5 + getPagedMemory(data, &obj.ChangeLog, &offset) return nil } func (obj *CodeVmAccount) String() string { return fmt.Sprintf( - "CodeVmAccount{authority=%s,mint=%s,omnibus=%s,lock_duration=%d,bump=%d,slot=%d,poh=%s}", + "CodeVmAccount{authority=%s,mint=%s,omnibus=%s,lock_duration=%d,bump=%d,slot=%d,poh=%s,changelog=%s}", base58.Encode(obj.Authority), base58.Encode(obj.Mint), obj.Omnibus.String(), @@ -67,5 +71,6 @@ func (obj *CodeVmAccount) String() string { obj.Bump, obj.Slot, obj.Poh.String(), + obj.ChangeLog.String(), ) } From e2b373837f31376c999e498f8f85be70a27da327 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 16 Sep 2024 13:48:35 -0700 Subject: [PATCH 40/79] Dynamic paged memory setup in VM accounts (#185) --- pkg/solana/cvm/accounts_code_vm.go | 10 ++-- pkg/solana/cvm/accounts_memory_account.go | 21 +++++-- pkg/solana/cvm/types_page.go | 37 ++++++++---- pkg/solana/cvm/types_paged_memory.go | 73 +++++++++++++++++++---- pkg/solana/cvm/types_sector.go | 33 +++++++--- 5 files changed, 137 insertions(+), 37 deletions(-) diff --git a/pkg/solana/cvm/accounts_code_vm.go b/pkg/solana/cvm/accounts_code_vm.go index 76e98e82..85807aa4 100644 --- a/pkg/solana/cvm/accounts_code_vm.go +++ b/pkg/solana/cvm/accounts_code_vm.go @@ -9,7 +9,8 @@ import ( ) const ( - CodeVmAccountSize = (8 + //discriminator + // todo: func for real size + minCodeVmAccountSize = (8 + //discriminator 32 + // authority 32 + // mint TokenPoolSize + // omnibus @@ -17,9 +18,7 @@ const ( 1 + // bump 8 + // slot HashSize + // poh - 5 + // padding - PagedMemorySize + // change_log - 2) // padding + 7) // padding ) var CodeVmAccountDiscriminator = []byte{0xed, 0x82, 0x60, 0x0b, 0xbb, 0x2c, 0xc7, 0x55} @@ -36,7 +35,7 @@ type CodeVmAccount struct { } func (obj *CodeVmAccount) Unmarshal(data []byte) error { - if len(data) < CodeVmAccountSize { + if len(data) < minCodeVmAccountSize { return ErrInvalidAccountData } @@ -56,6 +55,7 @@ func (obj *CodeVmAccount) Unmarshal(data []byte) error { getUint64(data, &obj.Slot, &offset) getHash(data, &obj.Poh, &offset) offset += 5 + obj.ChangeLog = NewChangelogMemory() getPagedMemory(data, &obj.ChangeLog, &offset) return nil diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index 03f1a658..289a0a4b 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -3,6 +3,7 @@ package cvm import ( "bytes" "crypto/ed25519" + "errors" "fmt" "github.com/mr-tron/base58" @@ -20,17 +21,17 @@ type MemoryAccountWithData struct { Data PagedMemory } -const MemoryAccountWithDataSize = (8 + // discriminator +// todo: func for real size +const minMemoryAccountWithDataSize = (8 + // discriminator 32 + // vm 1 + // bump MaxMemoryAccountNameLength + // name - 1 + // layout - PagedMemorySize) // data + 1) // todo: data var MemoryAccountDiscriminator = []byte{0x89, 0x7a, 0xdc, 0x6e, 0xdd, 0xca, 0x3e, 0x7f} func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { - if len(data) < MemoryAccountWithDataSize { + if len(data) < minMemoryAccountWithDataSize { return ErrInvalidAccountData } @@ -46,6 +47,18 @@ func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { getUint8(data, &obj.Bump, &offset) getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) getMemoryLayout(data, &obj.Layout, &offset) + switch obj.Layout { + case MemoryLayoutMixed: + obj.Data = NewMixedAccountMemory() + case MemoryLayoutTimelock: + obj.Data = NewTimelockAccountMemory() + case MemoryLayoutNonce: + obj.Data = NewNonceAccountMemory() + case MemoryLayoutRelay: + obj.Data = NewRelayAccountMemory() + default: + return errors.New("unexpected memory layout") + } getPagedMemory(data, &obj.Data, &offset) return nil diff --git a/pkg/solana/cvm/types_page.go b/pkg/solana/cvm/types_page.go index 40f005d2..0e32d188 100644 --- a/pkg/solana/cvm/types_page.go +++ b/pkg/solana/cvm/types_page.go @@ -1,32 +1,44 @@ package cvm -import "fmt" +import ( + "errors" + "fmt" +) const ( - PageDataLen = 77 + minPageSize = (1 + // is_allocated + 1) // NextPage ) -const PageSize = (1 + // is_allocated - PageDataLen + // data - 1) // NextPage - type Page struct { + dataLen uint32 + IsAllocated bool Data []byte NextPage uint8 } +func NewPage(dataLen uint32) Page { + return Page{ + dataLen: dataLen, + } +} + func (obj *Page) Unmarshal(data []byte) error { - if len(data) < PageSize { + if obj.dataLen == 0 { + return errors.New("page not initialized") + } + + if len(data) < int(GetPageSize(int(obj.dataLen))) { return ErrInvalidAccountData } var offset int - obj.Data = make([]byte, PageDataLen) + obj.Data = make([]byte, obj.dataLen) getBool(data, &obj.IsAllocated, &offset) - getBytes(data, obj.Data, PageDataLen, &offset) + getBytes(data, obj.Data, int(obj.dataLen), &offset) getUint8(data, &obj.NextPage, &offset) return nil @@ -43,5 +55,10 @@ func (obj *Page) String() string { func getPage(src []byte, dst *Page, offset *int) { dst.Unmarshal(src[*offset:]) - *offset += PageSize + *offset += int(GetPageSize(int(dst.dataLen))) +} + +func GetPageSize(dataLen int) int { + return (minPageSize + + dataLen) // page_size } diff --git a/pkg/solana/cvm/types_paged_memory.go b/pkg/solana/cvm/types_paged_memory.go index 30c0dff1..90e7d086 100644 --- a/pkg/solana/cvm/types_paged_memory.go +++ b/pkg/solana/cvm/types_paged_memory.go @@ -1,19 +1,63 @@ package cvm import ( + "errors" "fmt" "strings" ) const ( - PageCapacity = 100 - NumSectors = 2 + AccountMemoryCapacity = 100 // todo: set to 65536 + AccountMemorySectors = 2 // TODO: set to 255 + AccountMemoryPages = 255 + MixedAccountMemoryPageSize = 32 + + ChangelogMemoryCapacity = 255 + ChangelogMemorySectors = 2 + ChangelogMemoryPages = 180 + ChangelogMemoryPageSize = 21 ) -const PagedMemorySize = (PageCapacity*AllocatedMemorySize + // accounts - NumSectors*SectorSize) // sectors +func NewAccountMemory(accountPageSize uint32) PagedMemory { + return PagedMemory{ + capacity: AccountMemoryCapacity, + numSectors: AccountMemorySectors, + numPages: AccountMemoryPages, + pageSize: accountPageSize, + } +} + +func NewTimelockAccountMemory() PagedMemory { + return NewAccountMemory(GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock)) +} + +func NewNonceAccountMemory() PagedMemory { + return NewAccountMemory(GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce)) +} + +func NewRelayAccountMemory() PagedMemory { + return NewAccountMemory(GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay)) +} + +func NewMixedAccountMemory() PagedMemory { + return NewAccountMemory(MixedAccountMemoryPageSize) +} + +func NewChangelogMemory() PagedMemory { + return PagedMemory{ + capacity: ChangelogMemoryCapacity, + numSectors: ChangelogMemorySectors, + numPages: ChangelogMemoryPages, + pageSize: ChangelogMemoryPageSize, + } +} type PagedMemory struct { + capacity uint32 + numSectors uint32 + numPages uint32 + pageSize uint32 + Items []AllocatedMemory Sectors []Sector } @@ -43,19 +87,23 @@ func (obj *PagedMemory) Read(index int) ([]byte, bool) { } func (obj *PagedMemory) Unmarshal(data []byte) error { - if len(data) < PagedMemorySize { + if obj.capacity == 0 || obj.numSectors == 0 || obj.numPages == 0 || obj.pageSize == 0 { + return errors.New("paged memory not initialized") + } + + if len(data) < GetPagedMemorySize(int(obj.capacity), int(obj.numSectors), int(obj.numPages), int(obj.pageSize)) { return ErrInvalidAccountData } var offset int - obj.Items = make([]AllocatedMemory, PageCapacity) - obj.Sectors = make([]Sector, NumSectors) + obj.Items = make([]AllocatedMemory, obj.capacity) + obj.Sectors = make([]Sector, obj.numSectors) - for i := 0; i < PageCapacity; i++ { + for i := 0; i < int(obj.capacity); i++ { getAllocatedMemory(data, &obj.Items[i], &offset) } - for i := 0; i < NumSectors; i++ { + for i := 0; i < int(obj.numSectors); i++ { getSector(data, &obj.Sectors[i], &offset) } @@ -82,5 +130,10 @@ func (obj *PagedMemory) String() string { func getPagedMemory(src []byte, dst *PagedMemory, offset *int) { dst.Unmarshal(src[*offset:]) - *offset += PagedMemorySize + *offset += int(GetPagedMemorySize(int(dst.capacity), int(dst.numSectors), int(dst.numPages), int(dst.numPages))) +} + +func GetPagedMemorySize(capacity, numSectors, numPages, pageSize int) int { + return (capacity*AllocatedMemorySize + // accounts + numSectors*GetSectorSize(numPages, pageSize)) // sectors } diff --git a/pkg/solana/cvm/types_sector.go b/pkg/solana/cvm/types_sector.go index f1f35e9d..397e8e9f 100644 --- a/pkg/solana/cvm/types_sector.go +++ b/pkg/solana/cvm/types_sector.go @@ -1,22 +1,30 @@ package cvm import ( + "errors" "fmt" "strings" ) const ( - NumPages = 255 + minSectorSize = 1 // num_allocated ) -const SectorSize = (1 + // num_allocated - NumPages*PageSize) // pages - type Sector struct { + numPages uint32 + pageSize uint32 + NumAllocated uint8 Pages []Page } +func NewSector(numPages uint32, pageSize uint32) Sector { + return Sector{ + numPages: numPages, + pageSize: pageSize, + } +} + func (obj *Sector) GetLinkedPages(startIndex uint8) []Page { var res []Page current := startIndex @@ -31,16 +39,20 @@ func (obj *Sector) GetLinkedPages(startIndex uint8) []Page { } func (obj *Sector) Unmarshal(data []byte) error { - if len(data) < SectorSize { + if obj.numPages == 0 || obj.pageSize == 0 { + return errors.New("sector not initialized") + } + + if len(data) < GetSectorSize(int(obj.numPages), int(obj.pageSize)) { return ErrInvalidAccountData } var offset int - obj.Pages = make([]Page, NumPages) + obj.Pages = make([]Page, obj.numPages) getUint8(data, &obj.NumAllocated, &offset) - for i := 0; i < NumPages; i++ { + for i := 0; i < int(obj.numPages); i++ { getPage(data, &obj.Pages[i], &offset) } @@ -62,5 +74,10 @@ func (obj *Sector) String() string { func getSector(src []byte, dst *Sector, offset *int) { dst.Unmarshal(src[*offset:]) - *offset += SectorSize + *offset += int(GetSectorSize(int(dst.numPages), int(dst.pageSize))) +} + +func GetSectorSize(numPages, pageSize int) int { + return (minSectorSize + + numPages*pageSize) // pages } From bbc8aee2447eb417eba9a89a437f30197ba637be Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 16 Sep 2024 16:58:07 -0400 Subject: [PATCH 41/79] Fix comment --- pkg/solana/cvm/accounts_memory_account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index 289a0a4b..7d465e92 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -26,7 +26,7 @@ const minMemoryAccountWithDataSize = (8 + // discriminator 32 + // vm 1 + // bump MaxMemoryAccountNameLength + // name - 1) // todo: data + 1) // memory_layout var MemoryAccountDiscriminator = []byte{0x89, 0x7a, 0xdc, 0x6e, 0xdd, 0xca, 0x3e, 0x7f} From d9ce7734967def6d8ffa946b064264ece82c1efa Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 25 Sep 2024 12:33:22 -0400 Subject: [PATCH 42/79] Minor postgres DB changes --- pkg/code/data/cvm/ram/postgres/model.go | 2 +- pkg/code/data/cvm/storage/postgres/store_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/code/data/cvm/ram/postgres/model.go b/pkg/code/data/cvm/ram/postgres/model.go index edd06d2f..45188417 100644 --- a/pkg/code/data/cvm/ram/postgres/model.go +++ b/pkg/code/data/cvm/ram/postgres/model.go @@ -186,7 +186,7 @@ func dbReserveMemory(ctx context.Context, db *sqlx.DB, vm string, accountType cv SET is_allocated = true, address = $3, last_updated_at = $4 WHERE id IN ( SELECT id FROM ` + allocatedMemoryTableName + ` - WHERE vm = $1 AND NOT is_allocated AND stored_account_type = $2 + WHERE vm = $1 AND stored_account_type = $2 AND NOT is_allocated LIMIT 1 FOR UPDATE ) diff --git a/pkg/code/data/cvm/storage/postgres/store_test.go b/pkg/code/data/cvm/storage/postgres/store_test.go index a469212a..ff27a569 100644 --- a/pkg/code/data/cvm/storage/postgres/store_test.go +++ b/pkg/code/data/cvm/storage/postgres/store_test.go @@ -46,7 +46,7 @@ const ( vm TEXT NOT NULL, storage_account TEXT NOT NULL, - address TEXT NULL, + address TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, From 0d7aef1a23540e48974a0bf1bfd556b26b6c09e3 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 1 Oct 2024 10:54:35 -0400 Subject: [PATCH 43/79] Updates to cvm solana package --- pkg/solana/cvm/accounts_memory_account.go | 46 ++++++++++++++++++-- pkg/solana/cvm/program.go | 2 +- pkg/solana/cvm/types_memory_layout.go | 2 +- pkg/solana/cvm/types_paged_memory.go | 2 +- pkg/solana/cvm/types_virtual_account_type.go | 15 +++++++ 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index 7d465e92..50b0baa6 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -13,6 +13,13 @@ const ( MaxMemoryAccountNameLength = 32 ) +type MemoryAccount struct { + Vm ed25519.PublicKey + Bump uint8 + Name string + Layout MemoryLayout +} + type MemoryAccountWithData struct { Vm ed25519.PublicKey Bump uint8 @@ -21,8 +28,7 @@ type MemoryAccountWithData struct { Data PagedMemory } -// todo: func for real size -const minMemoryAccountWithDataSize = (8 + // discriminator +const MemoryAccountSize = (8 + // discriminator 32 + // vm 1 + // bump MaxMemoryAccountNameLength + // name @@ -30,8 +36,39 @@ const minMemoryAccountWithDataSize = (8 + // discriminator var MemoryAccountDiscriminator = []byte{0x89, 0x7a, 0xdc, 0x6e, 0xdd, 0xca, 0x3e, 0x7f} +func (obj *MemoryAccount) Unmarshal(data []byte) error { + if len(data) < MemoryAccountSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, MemoryAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.Vm, &offset) + getUint8(data, &obj.Bump, &offset) + getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) + getMemoryLayout(data, &obj.Layout, &offset) + + return nil +} + +func (obj *MemoryAccount) String() string { + return fmt.Sprintf( + "MemoryAccount{vm=%s,bump=%d,name=%s,layout=%d}", + base58.Encode(obj.Vm), + obj.Bump, + obj.Name, + obj.Layout, + ) +} + func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { - if len(data) < minMemoryAccountWithDataSize { + if len(data) < MemoryAccountSize { return ErrInvalidAccountData } @@ -66,10 +103,11 @@ func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { func (obj *MemoryAccountWithData) String() string { return fmt.Sprintf( - "MemoryAccountWithData{vm=%s,bump=%d,name=%s,data=%s}", + "MemoryAccountWithData{vm=%s,bump=%d,name=%s,layout=%d,data=%s}", base58.Encode(obj.Vm), obj.Bump, obj.Name, + obj.Layout, obj.Data.String(), ) } diff --git a/pkg/solana/cvm/program.go b/pkg/solana/cvm/program.go index 8a9e21c5..f585575d 100644 --- a/pkg/solana/cvm/program.go +++ b/pkg/solana/cvm/program.go @@ -15,7 +15,7 @@ var ( var ( // todo: setup real program address - PROGRAM_ADDRESS = mustBase58Decode("HzNbpGCu2S8fdbkVRJYnenj9BrSwW29tGuCQrgXdnmuc") + PROGRAM_ADDRESS = mustBase58Decode("vmTE1MUq7EBnZrXTLRRn2W9G2UMG6MEuh6UHngs3DuQ") PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) ) diff --git a/pkg/solana/cvm/types_memory_layout.go b/pkg/solana/cvm/types_memory_layout.go index 85d6dd27..92eb225b 100644 --- a/pkg/solana/cvm/types_memory_layout.go +++ b/pkg/solana/cvm/types_memory_layout.go @@ -29,7 +29,7 @@ func GetPageSizeFromMemoryLayout(layout MemoryLayout) uint32 { case MemoryLayoutTimelock: return GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock) case MemoryLayoutNonce: - return GetVirtualAccountSizeInMemory(VirtualDurableNonceSize) + return GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce) case MemoryLayoutRelay: return GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay) default: diff --git a/pkg/solana/cvm/types_paged_memory.go b/pkg/solana/cvm/types_paged_memory.go index 90e7d086..bf52f80b 100644 --- a/pkg/solana/cvm/types_paged_memory.go +++ b/pkg/solana/cvm/types_paged_memory.go @@ -8,7 +8,7 @@ import ( const ( AccountMemoryCapacity = 100 // todo: set to 65536 - AccountMemorySectors = 2 // TODO: set to 255 + AccountMemorySectors = 2 // todo: set to 255 AccountMemoryPages = 255 MixedAccountMemoryPageSize = 32 diff --git a/pkg/solana/cvm/types_virtual_account_type.go b/pkg/solana/cvm/types_virtual_account_type.go index 81e10b98..9bee24df 100644 --- a/pkg/solana/cvm/types_virtual_account_type.go +++ b/pkg/solana/cvm/types_virtual_account_type.go @@ -1,5 +1,7 @@ package cvm +import "errors" + type VirtualAccountType uint8 const ( @@ -7,3 +9,16 @@ const ( VirtualAccountTypeTimelock VirtualAccountTypeRelay ) + +func GetVirtualAccountTypeFromMemoryLayout(layout MemoryLayout) (VirtualAccountType, error) { + switch layout { + case MemoryLayoutNonce: + return VirtualAccountTypeDurableNonce, nil + case MemoryLayoutTimelock: + return VirtualAccountTypeTimelock, nil + case MemoryLayoutRelay: + return VirtualAccountTypeRelay, nil + default: + return 0, errors.New("memory layout doesn't have a defined virtual account type") + } +} From 6917f7baa4e1832b31b5c6a1d11843ec64af7e3e Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 1 Oct 2024 13:29:49 -0400 Subject: [PATCH 44/79] Fixes for VM paged memory unmarshalling --- pkg/solana/cvm/types_paged_memory.go | 8 +++++--- pkg/solana/cvm/types_sector.go | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/solana/cvm/types_paged_memory.go b/pkg/solana/cvm/types_paged_memory.go index bf52f80b..7308a874 100644 --- a/pkg/solana/cvm/types_paged_memory.go +++ b/pkg/solana/cvm/types_paged_memory.go @@ -98,7 +98,9 @@ func (obj *PagedMemory) Unmarshal(data []byte) error { var offset int obj.Items = make([]AllocatedMemory, obj.capacity) - obj.Sectors = make([]Sector, obj.numSectors) + for i := 0; i < int(obj.numSectors); i++ { + obj.Sectors = append(obj.Sectors, NewSector(obj.numPages, obj.pageSize)) + } for i := 0; i < int(obj.capacity); i++ { getAllocatedMemory(data, &obj.Items[i], &offset) @@ -117,8 +119,8 @@ func (obj *PagedMemory) String() string { } sectorStrings := make([]string, len(obj.Sectors)) - for i, page := range obj.Sectors { - sectorStrings[i] = page.String() + for i, sector := range obj.Sectors { + sectorStrings[i] = sector.String() } return fmt.Sprintf( diff --git a/pkg/solana/cvm/types_sector.go b/pkg/solana/cvm/types_sector.go index 397e8e9f..5bcc0f86 100644 --- a/pkg/solana/cvm/types_sector.go +++ b/pkg/solana/cvm/types_sector.go @@ -49,7 +49,9 @@ func (obj *Sector) Unmarshal(data []byte) error { var offset int - obj.Pages = make([]Page, obj.numPages) + for i := 0; i < int(obj.numPages); i++ { + obj.Pages = append(obj.Pages, NewPage(obj.pageSize)) + } getUint8(data, &obj.NumAllocated, &offset) for i := 0; i < int(obj.numPages); i++ { From 4b4b1b8528536b42418148d1113a2f6e1320f719 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 2 Oct 2024 11:08:16 -0400 Subject: [PATCH 45/79] Fix Solana mainnet nonce creation and optimize metric query counts --- pkg/code/async/nonce/allocator.go | 3 ++- pkg/code/async/nonce/metrics.go | 42 +++++++++++++++---------------- pkg/code/async/nonce/util.go | 12 +++++---- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/pkg/code/async/nonce/allocator.go b/pkg/code/async/nonce/allocator.go index d9240eb8..753957bd 100644 --- a/pkg/code/async/nonce/allocator.go +++ b/pkg/code/async/nonce/allocator.go @@ -69,8 +69,9 @@ func (p *service) generateNonceAccountsOnSolanaMainnet(serviceCtx context.Contex p.log.Warn("The nonce pool is too small.") } - _, err = p.createNonce(tracedCtx) + _, err = p.createSolanaMainnetNonce(tracedCtx) if err != nil { + p.log.WithError(err).Warn("failure creating nonce") return err } diff --git a/pkg/code/async/nonce/metrics.go b/pkg/code/async/nonce/metrics.go index b1846da4..b6dada11 100644 --- a/pkg/code/async/nonce/metrics.go +++ b/pkg/code/async/nonce/metrics.go @@ -25,30 +25,30 @@ func (p *service) metricsGaugeWorker(ctx context.Context) error { start := time.Now() // todo: optimize number of queries needed per polling check - for _, useCase := range []nonce.Purpose{ - nonce.PurposeClientTransaction, - nonce.PurposeInternalServerProcess, - nonce.PurposeOnDemandTransaction, + for _, state := range []nonce.State{ + nonce.StateUnknown, + nonce.StateReleased, + nonce.StateAvailable, + nonce.StateReserved, + nonce.StateInvalid, } { - for _, state := range []nonce.State{ - nonce.StateUnknown, - nonce.StateReleased, - nonce.StateAvailable, - nonce.StateReserved, - nonce.StateInvalid, - } { - count, err := p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, useCase) - if err != nil { - continue - } - recordNonceCountEvent(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, useCase, count) + count, err := p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), state, nonce.PurposeClientTransaction) + if err != nil { + continue + } + recordNonceCountEvent(ctx, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), state, nonce.PurposeClientTransaction, count) + + count, err = p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, nonce.PurposeOnDemandTransaction) + if err != nil { + continue + } + recordNonceCountEvent(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, nonce.PurposeOnDemandTransaction, count) - count, err = p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), state, useCase) - if err != nil { - continue - } - recordNonceCountEvent(ctx, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), state, useCase, count) + count, err = p.data.GetNonceCountByStateAndPurpose(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, nonce.PurposeInternalServerProcess) + if err != nil { + continue } + recordNonceCountEvent(ctx, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, state, nonce.PurposeInternalServerProcess, count) } delay = time.Second - time.Since(start) diff --git a/pkg/code/async/nonce/util.go b/pkg/code/async/nonce/util.go index bc6a7144..46f2a0a2 100644 --- a/pkg/code/async/nonce/util.go +++ b/pkg/code/async/nonce/util.go @@ -100,7 +100,7 @@ func (p *service) getRentAmount(ctx context.Context) (uint64, error) { return p.rent, nil } -func (p *service) createNonce(ctx context.Context) (*nonce.Record, error) { +func (p *service) createSolanaMainnetNonce(ctx context.Context) (*nonce.Record, error) { err := common.EnforceMinimumSubsidizerBalance(ctx, p.data) if err != nil { return nil, err @@ -112,10 +112,12 @@ func (p *service) createNonce(ctx context.Context) (*nonce.Record, error) { } res := nonce.Record{ - Address: key.PublicKey, - Authority: common.GetSubsidizer().PublicKey().ToBase58(), - Purpose: nonce.PurposeClientTransaction, // todo: intelligently set a purpose - State: nonce.StateUnknown, + Address: key.PublicKey, + Authority: common.GetSubsidizer().PublicKey().ToBase58(), + Environment: nonce.EnvironmentSolana, + EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, + Purpose: nonce.PurposeOnDemandTransaction, // todo: intelligently set a purpose, but most use cases require this type of nonce + State: nonce.StateUnknown, } tx, err := p.createNonceAccountTx(ctx, &res) From c9681480cb1c68c4f33b22f4d474aeabc77bbfd3 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 10 Oct 2024 10:02:53 -0400 Subject: [PATCH 46/79] Fix nonce signature updates in SubmitIntent --- pkg/code/server/grpc/transaction/v2/intent.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/code/server/grpc/transaction/v2/intent.go b/pkg/code/server/grpc/transaction/v2/intent.go index e05e307d..4490c6ed 100644 --- a/pkg/code/server/grpc/transaction/v2/intent.go +++ b/pkg/code/server/grpc/transaction/v2/intent.go @@ -773,9 +773,9 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm if fulfillmentWithMetadata.requiresClientSignature { nonceToReserve := reservedNonces[i] if isIntentUpdateOperation { - err = nonceToReserve.UpdateSignature(ctx, *fulfillmentWithMetadata.record.Signature) + err = nonceToReserve.UpdateSignature(ctx, *fulfillmentWithMetadata.record.VirtualSignature) } else { - err = nonceToReserve.MarkReservedWithSignature(ctx, *fulfillmentWithMetadata.record.Signature) + err = nonceToReserve.MarkReservedWithSignature(ctx, *fulfillmentWithMetadata.record.VirtualSignature) } if err != nil { log.WithError(err).Warn("failure reserving nonce with fulfillment signature") From e3e133f1d2f212774e70305044c351fb4110dab1 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 10 Oct 2024 10:03:21 -0400 Subject: [PATCH 47/79] Fix VM opcodes --- pkg/solana/cvm/types_opcode.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index 8e7cd7ca..5db22881 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -3,14 +3,14 @@ package cvm type Opcode uint8 const ( - OpcodeTimelockTransferToInternal Opcode = 10 - OpcodeTimelockTransferToExternal Opcode = 11 + OpcodeTimelockTransferToExternal Opcode = 10 + OpcodeTimelockTransferToInternal Opcode = 11 OpcodeTimelockTransferToRelay Opcode = 12 - OpcodeTimelockWithdrawToInternal Opcode = 13 - OpcodeTimelockWithdrawToExternal Opcode = 14 + OpcodeTimelockWithdrawToExternal Opcode = 13 + OpcodeTimelockWithdrawToInternal Opcode = 14 - OpcodeSplitterTransferToInternal Opcode = 20 - OpcodeSplitterTransferToExternal Opcode = 21 + OpcodeSplitterTransferToExternal Opcode = 20 + OpcodeSplitterTransferToInternal Opcode = 21 ) func putOpcode(dst []byte, v Opcode, offset *int) { From dcc80ce33d991f4a4553382a4acf09a793243ad2 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 11 Oct 2024 09:39:00 -0400 Subject: [PATCH 48/79] Merge memory banks used in VM exec instruction --- pkg/code/transaction/transaction.go | 127 ++++++++++++++++------- pkg/code/transaction/transaction_test.go | 25 ++++- 2 files changed, 112 insertions(+), 40 deletions(-) diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index ee4dc9cb..fd02dff4 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -1,6 +1,7 @@ package transaction import ( + "bytes" "crypto/ed25519" "crypto/sha256" "errors" @@ -105,9 +106,10 @@ func MakeInternalWithdrawTransaction( source *common.TimelockAccounts, destination *common.Account, ) (solana.Transaction, error) { - memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) - memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) - memoryCPublicKeyBytes := ed25519.PublicKey(destinationMemory.PublicKey().ToBytes()) + mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) + if err != nil { + return solana.Transaction{}, err + } vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), @@ -135,14 +137,14 @@ func MakeInternalWithdrawTransaction( &cvm.VmExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), - VmMemA: &memoryAPublicKeyBytes, - VmMemB: &memoryBPublicKeyBytes, - VmMemC: &memoryCPublicKeyBytes, + VmMemA: mergedMemoryBanks.A, + VmMemB: mergedMemoryBanks.B, + VmMemC: mergedMemoryBanks.C, }, &cvm.VmExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, - MemBanks: []uint8{0, 1, 2}, + MemBanks: mergedMemoryBanks.Indices, Data: vixn.Data, }, ) @@ -167,8 +169,10 @@ func MakeExternalWithdrawTransaction( source *common.TimelockAccounts, destination *common.Account, ) (solana.Transaction, error) { - memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) - memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) + mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory) + if err != nil { + return solana.Transaction{}, err + } destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) @@ -198,14 +202,14 @@ func MakeExternalWithdrawTransaction( &cvm.VmExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), - VmMemA: &memoryAPublicKeyBytes, - VmMemB: &memoryBPublicKeyBytes, + VmMemA: mergedMemoryBanks.A, + VmMemB: mergedMemoryBanks.B, ExternalAddress: &destinationPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex}, - MemBanks: []uint8{0, 1}, + MemBanks: mergedMemoryBanks.Indices, Data: vixn.Data, }, ) @@ -233,9 +237,10 @@ func MakeInternalTransferWithAuthorityTransaction( destination *common.Account, kinAmountInQuarks uint64, ) (solana.Transaction, error) { - memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) - memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) - memoryCPublicKeyBytes := ed25519.PublicKey(destinationMemory.PublicKey().ToBytes()) + mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) + if err != nil { + return solana.Transaction{}, err + } vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), @@ -263,14 +268,14 @@ func MakeInternalTransferWithAuthorityTransaction( &cvm.VmExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), - VmMemA: &memoryAPublicKeyBytes, - VmMemB: &memoryBPublicKeyBytes, - VmMemC: &memoryCPublicKeyBytes, + VmMemA: mergedMemoryBanks.A, + VmMemB: mergedMemoryBanks.B, + VmMemC: mergedMemoryBanks.C, }, &cvm.VmExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, - MemBanks: []uint8{0, 1, 2}, + MemBanks: mergedMemoryBanks.Indices, Data: vixn.Data, }, ) @@ -296,8 +301,10 @@ func MakeExternalTransferWithAuthorityTransaction( destination *common.Account, kinAmountInQuarks uint64, ) (solana.Transaction, error) { - memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) - memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) + mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory) + if err != nil { + return solana.Transaction{}, err + } destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) @@ -327,14 +334,14 @@ func MakeExternalTransferWithAuthorityTransaction( &cvm.VmExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), - VmMemA: &memoryAPublicKeyBytes, - VmMemB: &memoryBPublicKeyBytes, + VmMemA: mergedMemoryBanks.A, + VmMemB: mergedMemoryBanks.B, ExternalAddress: &destinationPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex}, - MemBanks: []uint8{0, 1}, + MemBanks: mergedMemoryBanks.Indices, Data: vixn.Data, }, ) @@ -360,12 +367,14 @@ func MakeInternalTreasuryAdvanceTransaction( transcript []byte, recentRoot []byte, ) (solana.Transaction, error) { - memoryAPublicKeyBytes := ed25519.PublicKey(accountMemory.PublicKey().ToBytes()) - memoryBPublicKeyBytes := ed25519.PublicKey(relayMemory.PublicKey().ToBytes()) - treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) + mergedMemoryBanks, err := mergeMemoryBanks(accountMemory, relayMemory) + if err != nil { + return solana.Transaction{}, err + } + vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), nil, @@ -384,15 +393,15 @@ func MakeInternalTreasuryAdvanceTransaction( &cvm.VmExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), - VmMemA: &memoryAPublicKeyBytes, - VmMemB: &memoryBPublicKeyBytes, + VmMemA: mergedMemoryBanks.A, + VmMemB: mergedMemoryBanks.B, VmRelay: &treasuryPoolPublicKeyBytes, VmRelayVault: &treasuryPoolVaultPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{accountIndex, relayIndex}, - MemBanks: []uint8{0, 1}, + MemBanks: mergedMemoryBanks.Indices, Data: vixn.Data, }, ) @@ -483,13 +492,14 @@ func MakeCashChequeTransaction( ) (solana.Transaction, error) { vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) - memoryAPublicKeyBytes := ed25519.PublicKey(nonceMemory.PublicKey().ToBytes()) - memoryBPublicKeyBytes := ed25519.PublicKey(sourceMemory.PublicKey().ToBytes()) - memoryCPublicKeyBytes := ed25519.PublicKey(relayMemory.PublicKey().ToBytes()) - treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) + mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory, relayMemory) + if err != nil { + return solana.Transaction{}, err + } + vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), &cvm.VirtualDurableNonce{ @@ -516,9 +526,9 @@ func MakeCashChequeTransaction( &cvm.VmExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), - VmMemA: &memoryAPublicKeyBytes, - VmMemB: &memoryBPublicKeyBytes, - VmMemC: &memoryCPublicKeyBytes, + VmMemA: mergedMemoryBanks.A, + VmMemB: mergedMemoryBanks.B, + VmMemC: mergedMemoryBanks.C, VmOmnibus: &vmOmnibusPublicKeyBytes, VmRelay: &treasuryPoolPublicKeyBytes, VmRelayVault: &treasuryPoolVaultPublicKeyBytes, @@ -527,10 +537,51 @@ func MakeCashChequeTransaction( &cvm.VmExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, relayIndex}, - MemBanks: []uint8{0, 1, 2}, + MemBanks: mergedMemoryBanks.Indices, Data: vixn.Data, }, ) return MakeNoncedTransaction(nonce, bh, execInstruction) } + +type mergedMemoryBankResult struct { + A *ed25519.PublicKey + B *ed25519.PublicKey + C *ed25519.PublicKey + D *ed25519.PublicKey + Indices []uint8 +} + +func mergeMemoryBanks(accounts ...*common.Account) (*mergedMemoryBankResult, error) { + indices := make([]uint8, len(accounts)) + orderedBanks := make([]*ed25519.PublicKey, 4) + + for i, account := range accounts { + for j, bank := range orderedBanks { + if bank == nil { + publicKey := ed25519.PublicKey(account.PublicKey().ToBytes()) + orderedBanks[j] = &publicKey + indices[i] = uint8(j) + break + } + + if bytes.Equal(*bank, account.PublicKey().ToBytes()) { + indices[i] = uint8(j) + break + } + + if j == len(orderedBanks)-1 { + return nil, errors.New("too many memory banks") + } + } + } + + return &mergedMemoryBankResult{ + A: orderedBanks[0], + B: orderedBanks[1], + C: orderedBanks[2], + D: orderedBanks[3], + Indices: indices, + }, nil +} diff --git a/pkg/code/transaction/transaction_test.go b/pkg/code/transaction/transaction_test.go index 811d7439..9011bb90 100644 --- a/pkg/code/transaction/transaction_test.go +++ b/pkg/code/transaction/transaction_test.go @@ -7,12 +7,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/system" "github.com/code-payments/code-server/pkg/solana/token" "github.com/code-payments/code-server/pkg/testutil" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" ) func TestTransaction_MakeNoncedTransaction_HappyPath(t *testing.T) { @@ -78,3 +78,24 @@ func TestTransaction_MakeNoncedTransaction_NoInstructions(t *testing.T) { _, err = MakeNoncedTransaction(nonceAccount, typedBlockhash) assert.Error(t, err) } + +func TestVmTransaction_MergedMemoryBanks_HappyPath(t *testing.T) { + account1 := testutil.NewRandomAccount(t) + account2 := testutil.NewRandomAccount(t) + account3 := testutil.NewRandomAccount(t) + + res, err := mergeMemoryBanks(account1, account1, account2, account3, account2) + require.NoError(t, err) + + assert.EqualValues(t, account1.PublicKey().ToBytes(), *res.A) + assert.EqualValues(t, account2.PublicKey().ToBytes(), *res.B) + assert.EqualValues(t, account3.PublicKey().ToBytes(), *res.C) + assert.Nil(t, res.D) + + assert.Equal(t, []uint8{0, 0, 1, 2, 1}, res.Indices) +} + +func TestVmTransaction_MergedMemoryBanks_TooManyAccounts(t *testing.T) { + _, err := mergeMemoryBanks(testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t)) + assert.Error(t, err) +} From c49cb2f4526b4bfb33bf1ccd2e8d7950b3fac49c Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 11 Oct 2024 09:39:29 -0400 Subject: [PATCH 49/79] Virtual transaction hash should be calculated from the transaction message --- pkg/code/transaction/virtual_instruction.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/code/transaction/virtual_instruction.go b/pkg/code/transaction/virtual_instruction.go index e7cf2dd7..706e9792 100644 --- a/pkg/code/transaction/virtual_instruction.go +++ b/pkg/code/transaction/virtual_instruction.go @@ -129,6 +129,6 @@ func GetVirtualWithdrawTransaction( func getVirtualTransactionHash(txn *solana.Transaction) cvm.Hash { hasher := sha256.New() - hasher.Write(txn.Marshal()) + hasher.Write(txn.Message.Marshal()) return cvm.Hash(hasher.Sum(nil)) } From 44a7d18fa38b230c98eea14cea6fef1f0d456d98 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 11 Oct 2024 10:33:34 -0400 Subject: [PATCH 50/79] Fix external transfer with authority transaction contruction --- pkg/code/transaction/transaction.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index fd02dff4..2f8041a2 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -314,15 +314,15 @@ func MakeExternalTransferWithAuthorityTransaction( Address: virtualNonce.PublicKey().ToBytes(), Nonce: cvm.Hash(virtualBlockhash), }, - cvm.NewTimelockTransferInternalVirtualInstructionCtor( - &cvm.TimelockTransferInternalVirtualInstructionAccounts{ + cvm.NewTimelockTransferExternalVirtualInstructionCtor( + &cvm.TimelockTransferExternalVirtualInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), VirtualTimelock: source.State.PublicKey().ToBytes(), VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), Owner: source.VaultOwner.PublicKey().ToBytes(), Destination: destination.PublicKey().ToBytes(), }, - &cvm.TimelockTransferInternalVirtualInstructionArgs{ + &cvm.TimelockTransferExternalVirtualInstructionArgs{ TimelockBump: source.StateBump, Amount: kinAmountInQuarks, Signature: cvm.Signature(virtualSignature), From e9d85b67d9054af363c9cc8d3382653fe88cd6dc Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 11 Oct 2024 10:42:20 -0400 Subject: [PATCH 51/79] Ensure VM omnibus is provided for external transfers --- pkg/code/async/sequencer/fulfillment_handler.go | 4 ++++ pkg/code/transaction/transaction.go | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index ce2b4b90..85f9c545 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -389,6 +389,8 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti solana.Blockhash(virtualBlockhashBytes), common.CodeVmAccount, + common.CodeVmOmnibusAccount, + nonceMemory, nonceIndex, sourceMemory, @@ -574,6 +576,8 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex solana.Blockhash(virtualBlockhashBytes), common.CodeVmAccount, + common.CodeVmOmnibusAccount, + nonceMemory, nonceIndex, sourceMemory, diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index 2f8041a2..ac6fd0c5 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -161,6 +161,8 @@ func MakeExternalWithdrawTransaction( virtualBlockhash solana.Blockhash, vm *common.Account, + vmOmnibus *common.Account, + nonceMemory *common.Account, nonceIndex uint16, sourceMemory *common.Account, @@ -174,6 +176,8 @@ func MakeExternalWithdrawTransaction( return solana.Transaction{}, err } + vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) + destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) vixn := cvm.NewVirtualInstruction( @@ -204,6 +208,7 @@ func MakeExternalWithdrawTransaction( Vm: vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, + VmOmnibus: &vmOmnibusPublicKeyBytes, ExternalAddress: &destinationPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ @@ -292,6 +297,8 @@ func MakeExternalTransferWithAuthorityTransaction( virtualBlockhash solana.Blockhash, vm *common.Account, + vmOmnibus *common.Account, + nonceMemory *common.Account, nonceIndex uint16, sourceMemory *common.Account, @@ -308,6 +315,8 @@ func MakeExternalTransferWithAuthorityTransaction( destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) + vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) + vixn := cvm.NewVirtualInstruction( common.GetSubsidizer().PublicKey().ToBytes(), &cvm.VirtualDurableNonce{ @@ -336,6 +345,7 @@ func MakeExternalTransferWithAuthorityTransaction( Vm: vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, + VmOmnibus: &vmOmnibusPublicKeyBytes, ExternalAddress: &destinationPublicKeyBytes, }, &cvm.VmExecInstructionArgs{ From 1e611a41cf767ac6f567ea94d47966e2794ac0f8 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 15 Oct 2024 10:36:36 -0400 Subject: [PATCH 52/79] Keep virtual nonce reserved after fulfillment failure --- pkg/code/async/sequencer/utils.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/code/async/sequencer/utils.go b/pkg/code/async/sequencer/utils.go index 821ec816..a204b2e3 100644 --- a/pkg/code/async/sequencer/utils.go +++ b/pkg/code/async/sequencer/utils.go @@ -63,11 +63,6 @@ func (p *service) markFulfillmentFailed(ctx context.Context, record *fulfillment return err } - err = p.markVirtualNonceReleasedDueToSubmittedTransaction(ctx, record) - if err != nil { - return err - } - record.State = fulfillment.StateFailed record.Data = nil return p.data.UpdateFulfillment(ctx, record) From 7da932d0f18208c627a7b34cb35eb30d5679d77f Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 15 Oct 2024 14:05:03 -0400 Subject: [PATCH 53/79] Fix welcome bonus airdrop --- .../server/grpc/transaction/v2/airdrop.go | 212 ++++-------------- .../server/grpc/transaction/v2/metrics.go | 3 +- 2 files changed, 45 insertions(+), 170 deletions(-) diff --git a/pkg/code/server/grpc/transaction/v2/airdrop.go b/pkg/code/server/grpc/transaction/v2/airdrop.go index f003a424..cb0d08aa 100644 --- a/pkg/code/server/grpc/transaction/v2/airdrop.go +++ b/pkg/code/server/grpc/transaction/v2/airdrop.go @@ -2,15 +2,37 @@ package transaction_v2 import ( "context" + "crypto/ed25519" "crypto/sha256" + "database/sql" "fmt" + "time" "github.com/mr-tron/base58/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" + transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" "github.com/code-payments/code-server/pkg/cache" + "github.com/code-payments/code-server/pkg/code/balance" "github.com/code-payments/code-server/pkg/code/common" + "github.com/code-payments/code-server/pkg/code/data/account" + "github.com/code-payments/code-server/pkg/code/data/action" + "github.com/code-payments/code-server/pkg/code/data/event" + "github.com/code-payments/code-server/pkg/code/data/fulfillment" + "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/code/data/nonce" + event_util "github.com/code-payments/code-server/pkg/code/event" + exchange_rate_util "github.com/code-payments/code-server/pkg/code/exchangerate" + "github.com/code-payments/code-server/pkg/code/transaction" + currency_lib "github.com/code-payments/code-server/pkg/currency" + "github.com/code-payments/code-server/pkg/grpc/client" + "github.com/code-payments/code-server/pkg/kin" + "github.com/code-payments/code-server/pkg/pointer" ) // This is a quick and dirty file to get an initial airdrop feature out @@ -37,12 +59,9 @@ var ( ) var ( - cachedAirdropStatus = cache.NewCache(10_000) - cachedFirstReceivesByOwner = cache.NewCache(10_000) + cachedAirdropStatus = cache.NewCache(10_000) ) -/* - func (s *transactionServer) Airdrop(ctx context.Context, req *transactionpb.AirdropRequest) (*transactionpb.AirdropResponse, error) { log := s.log.WithFields(logrus.Fields{ "method": "Airdrop", @@ -155,7 +174,7 @@ func (s *transactionServer) Airdrop(ctx context.Context, req *transactionpb.Aird }, nil } -// airdrop gives Kin airdrops denominated in USD for performing certain +// airdrop gives Kin airdrops denominated in Kin for performing certain // actions in the Code app. This funciton is idempotent with the given // intent ID. func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner *common.Account, airdropType AirdropType) (*intent.Record, error) { @@ -173,15 +192,14 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner return nil, err } - var usdValue float64 + var quarkAmount uint64 switch airdropType { - case AirdropTypeGiveFirstKin: - usdValue = 5.0 case AirdropTypeGetFirstKin: - usdValue = 1.0 + quarkAmount = kin.ToQuarks(1) default: return nil, errors.New("unhandled airdrop type") } + kinAmount := float64(kin.FromQuarks(quarkAmount)) // Find the destination account, which will be the user's primary account primaryAccountInfoRecord, err := s.data.GetLatestAccountInfoByOwnerAddressAndType(ctx, owner.PublicKey().ToBase58(), commonpb.AccountType_PRIMARY) @@ -198,16 +216,11 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner return nil, err } - // Calculate the amount of quarks to send usdRateRecord, err := s.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) if err != nil { log.WithError(err).Warn("failure getting usd rate") return nil, err } - quarks := kin.ToQuarks(uint64(usdValue / usdRateRecord.Rate)) - - // Add an additional Kin, so we can always have enough to send the full fiat amount - quarks += kin.ToQuarks(1) var isAirdropperManuallyUnlocked bool s.airdropperLock.Lock() @@ -232,10 +245,10 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner if err != nil { log.WithError(err).Warn("failure getting airdropper balance") return nil, err - } else if balance < quarks { + } else if balance < quarkAmount { log.WithFields(logrus.Fields{ "balance": balance, - "required": quarks, + "required": quarkAmount, }).Warn("airdropper has insufficient balance") return nil, ErrInsufficientAirdropperBalance } @@ -248,7 +261,7 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner // Instead of constructing and validating everything manually, we could // have a proper client call SubmitIntent in a worker. - selectedNonce, err := transaction.SelectAvailableNonce(ctx, s.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeClientTransaction) + selectedNonce, err := transaction.SelectAvailableNonce(ctx, s.data, nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), nonce.PurposeClientTransaction) if err != nil { log.WithError(err).Warn("failure selecting available nonce") return nil, err @@ -258,23 +271,18 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner selectedNonce.Unlock() }() - txn, err := transaction.MakeTransferWithAuthorityTransaction( + vixnHash, err := transaction.GetVirtualTransferWithAuthorityHash( selectedNonce.Account, selectedNonce.Blockhash, s.airdropper, destination, - quarks, + quarkAmount, ) if err != nil { log.WithError(err).Warn("failure making solana transaction") return nil, err } - - err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes(), s.airdropper.VaultOwner.PrivateKey().ToBytes()) - if err != nil { - log.WithError(err).Warn("failure signing solana transaction") - return nil, err - } + virtualSig := ed25519.Sign(s.airdropper.VaultOwner.PrivateKey().ToBytes(), vixnHash[:]) intentRecord := &intent.Record{ IntentId: intentId, @@ -283,12 +291,12 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ DestinationOwnerAccount: owner.PublicKey().ToBase58(), DestinationTokenAccount: destination.PublicKey().ToBase58(), - Quantity: quarks, + Quantity: quarkAmount, - ExchangeCurrency: currency_lib.USD, - ExchangeRate: usdRateRecord.Rate, - NativeAmount: usdValue, - UsdMarketValue: usdValue, + ExchangeCurrency: currency_lib.KIN, + ExchangeRate: 1.0, + NativeAmount: kinAmount, + UsdMarketValue: usdRateRecord.Rate * kinAmount, IsWithdrawal: true, }, @@ -310,7 +318,7 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner Source: s.airdropper.Vault.PublicKey().ToBase58(), Destination: pointer.String(destination.PublicKey().ToBase58()), - Quantity: pointer.Uint64(quarks), + Quantity: pointer.Uint64(quarkAmount), State: action.StatePending, @@ -325,11 +333,10 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner ActionType: actionRecord.ActionType, FulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, - Data: txn.Marshal(), - Signature: pointer.String(base58.Encode(txn.Signature())), - Nonce: pointer.String(selectedNonce.Account.PublicKey().ToBase58()), - Blockhash: pointer.String(base58.Encode(selectedNonce.Blockhash[:])), + VirtualNonce: pointer.String(selectedNonce.Account.PublicKey().ToBase58()), + VirtualBlockhash: pointer.String(base58.Encode(selectedNonce.Blockhash[:])), + VirtualSignature: pointer.String(base58.Encode(virtualSig)), Source: actionRecord.Source, Destination: pointer.StringCopy(actionRecord.Destination), @@ -366,23 +373,6 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner } event_util.InjectClientDetails(ctx, s.maxmind, eventRecord, true) - var chatMessage *chatpb.ChatMessage - switch airdropType { - case AirdropTypeGetFirstKin: - chatMessage, err = chat_util.ToWelcomeBonusMessage(intentRecord) - if err != nil { - return nil, err - } - case AirdropTypeGiveFirstKin: - chatMessage, err = chat_util.ToReferralBonusMessage(intentRecord) - if err != nil { - return nil, err - } - default: - return nil, errors.Errorf("no chat message defined for %s airdrop", airdropType.String()) - } - - var canPushChatMessage bool err = s.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { err := s.data.SaveIntent(ctx, intentRecord) if err != nil { @@ -400,7 +390,7 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner return err } - err = selectedNonce.MarkReservedWithSignature(ctx, *fulfillmentRecord.Signature) + err = selectedNonce.MarkReservedWithSignature(ctx, *fulfillmentRecord.VirtualSignature) if err != nil { return err } @@ -410,11 +400,6 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner return err } - canPushChatMessage, err = chat_util.SendCodeTeamMessage(ctx, s.data, owner, chatMessage) - if err != nil { - return err - } - // Intent is pending only after everything's been saved. intentRecord.State = intent.StatePending return s.data.SaveIntent(ctx, intentRecord) @@ -428,120 +413,11 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner s.airdropperLock.Unlock() isAirdropperManuallyUnlocked = true - if canPushChatMessage { - // Best-effort send a push - push_util.SendChatMessagePushNotification( - ctx, - s.data, - s.pusher, - chat_util.CodeTeamName, - owner, - chatMessage, - ) - } - - recordAirdropEvent(ctx, owner, airdropType, usdValue) + recordAirdropEvent(ctx, owner, airdropType) return intentRecord, nil } -func (s *transactionServer) isFirstReceiveFromOtherCodeUser(ctx context.Context, intentToCheck string, owner *common.Account) (bool, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "isFirstReceiveFromOtherCodeUser", - "intent": intentToCheck, - "owner": owner.PublicKey().ToBase58(), - }) - - cached, ok := cachedFirstReceivesByOwner.Retrieve(owner.PublicKey().ToBase58()) - if ok { - return intentToCheck == cached.(string), nil - } - - verificationRecord, err := s.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting phone verification record") - return false, err - } - log = log.WithField("phone", verificationRecord.PhoneNumber) - - // Staff are excluded since they can have multiple accounts per phone number, - // and shouldn't be counted regardless. - userIdentityRecord, err := s.data.GetUserByPhoneView(ctx, verificationRecord.PhoneNumber) - if err != nil { - log.WithError(err).Warn("failure getting user identity record") - return false, err - } else if userIdentityRecord.IsStaffUser { - return false, nil - } - - // Take a small view of the user's initial intent history from the very beginning - history, err := s.data.GetAllIntentsByOwner( - ctx, - owner.PublicKey().ToBase58(), - query.WithLimit(100), - query.WithDirection(query.Ascending), - ) - if err != nil { - log.WithError(err).Warn("failure loading owner history") - return false, err - } - - var firstReceive *intent.Record - createdGiftCards := make(map[string]struct{}) - for _, historyItem := range history { - switch historyItem.IntentType { - case intent.SendPrivatePayment, intent.SendPublicPayment: - // The owner iniated the send - if historyItem.InitiatorOwnerAccount == owner.PublicKey().ToBase58() { - // Keep track of any gift cards the user created for future gift card receives - if historyItem.IntentType == intent.SendPrivatePayment && historyItem.SendPrivatePaymentMetadata.IsRemoteSend { - createdGiftCards[historyItem.SendPrivatePaymentMetadata.DestinationTokenAccount] = struct{}{} - } - - continue - } - - // The airdropper initiated the send - if historyItem.InitiatorOwnerAccount == s.airdropper.VaultOwner.PublicKey().ToBase58() { - continue - } - - firstReceive = historyItem - case intent.ReceivePaymentsPublicly: - // Not receiving a gift card - if !historyItem.ReceivePaymentsPubliclyMetadata.IsRemoteSend { - continue - } - - _, ok := createdGiftCards[historyItem.ReceivePaymentsPubliclyMetadata.Source] - if !ok { - // User didn't create this gift card - firstReceive = historyItem - } - case intent.MigrateToPrivacy2022: - // User received their first Kin at some point with pre-privacy account - if historyItem.MigrateToPrivacy2022Metadata.Quantity > 0 { - firstReceive = historyItem - } - } - - if firstReceive != nil { - break - } - } - - // Highly unlikely to happen. At this point we can say the user onboarded - // themself sufficiently anways. - if firstReceive == nil { - return false, nil - } - - cachedFirstReceivesByOwner.Insert(owner.PublicKey().ToBase58(), firstReceive.IntentId, 1) - return firstReceive.IntentId == intentToCheck, nil -} - -*/ - func (s *transactionServer) mustLoadAirdropper(ctx context.Context) { log := s.log.WithFields(logrus.Fields{ "method": "mustLoadAirdropper", diff --git a/pkg/code/server/grpc/transaction/v2/metrics.go b/pkg/code/server/grpc/transaction/v2/metrics.go index 4cc302cc..52bfe975 100644 --- a/pkg/code/server/grpc/transaction/v2/metrics.go +++ b/pkg/code/server/grpc/transaction/v2/metrics.go @@ -45,11 +45,10 @@ func recordSubmitIntentLatencyBreakdownEvent(ctx context.Context, section string }) } -func recordAirdropEvent(ctx context.Context, owner *common.Account, airdropType AirdropType, usdValue float64) { +func recordAirdropEvent(ctx context.Context, owner *common.Account, airdropType AirdropType) { metrics.RecordEvent(ctx, airdropEventName, map[string]interface{}{ "owner": owner.PublicKey().ToBase58(), "airdrop_type": airdropType.String(), - "usd_value": usdValue, }) } From b7ba55e96da3df137fef54b8bab2eac722a0a99c Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 17 Oct 2024 09:14:22 -0400 Subject: [PATCH 54/79] Fix treasury pool record update method --- pkg/code/data/treasury/treasury.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/code/data/treasury/treasury.go b/pkg/code/data/treasury/treasury.go index b0d72d35..eeb7dfca 100644 --- a/pkg/code/data/treasury/treasury.go +++ b/pkg/code/data/treasury/treasury.go @@ -118,7 +118,7 @@ func (r *Record) Update(data *cvm.RelayAccount, solanaBlock uint64) error { if r.CurrentIndex == data.RecentRoots.Offset { var hasUpdatedHistoryList bool - for i := 0; i < int(r.HistoryListSize); i++ { + for i := 0; i < len(data.RecentRoots.Items); i++ { if r.HistoryList[i] != data.RecentRoots.Items[i].String() { hasUpdatedHistoryList = true break @@ -138,6 +138,11 @@ func (r *Record) Update(data *cvm.RelayAccount, solanaBlock uint64) error { for i, recentRoot := range data.RecentRoots.Items { historyList[i] = recentRoot.String() } + for i, hash := range historyList { + if len(hash) == 0 { + historyList[i] = historyList[0] + } + } r.HistoryList = historyList r.SolanaBlock = solanaBlock From 3679f231c39455ebc67213fe39c2893319a03a54 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 17 Oct 2024 10:10:54 -0400 Subject: [PATCH 55/79] Fixes for VM storage worker --- pkg/code/async/vm/storage.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/code/async/vm/storage.go b/pkg/code/async/vm/storage.go index c77aed0d..e2e430a2 100644 --- a/pkg/code/async/vm/storage.go +++ b/pkg/code/async/vm/storage.go @@ -98,7 +98,7 @@ func (p *service) initStorageAccountOnBlockchain(ctx context.Context, vm *common txn := solana.NewTransaction( common.GetSubsidizer().PublicKey().ToBytes(), - compute_budget.SetComputeUnitLimit(10_000), + compute_budget.SetComputeUnitLimit(100_000), compute_budget.SetComputeUnitPrice(10_000), cvm.NewVmStorageInitInstruction( &cvm.VmStorageInitInstructionAccounts{ @@ -132,8 +132,8 @@ func (p *service) initStorageAccountOnBlockchain(ctx context.Context, vm *common time.Sleep(4 * time.Second) - _, err = p.data.GetBlockchainTransaction(ctx, base58.Encode(sig[:]), solana.CommitmentFinalized) - if err == nil { + finalizedTxn, err := p.data.GetBlockchainTransaction(ctx, base58.Encode(sig[:]), solana.CommitmentFinalized) + if err == nil && finalizedTxn.Err == nil && finalizedTxn.Meta.Err == nil { return record, nil } } From 2ec566997d05c5f5b2f9c2504b943a6f573bace1 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Mon, 28 Oct 2024 09:25:28 -0400 Subject: [PATCH 56/79] VM without anchor (#188) * Define anchorless VM enums * Update PDAs * Define unkown memory layout * Update virtual relay account state * Define virtual instruction messages * Update virtual instructions * Remove ATA deposit instruction * Remove instructions system variable * Update filename for consistency * Update instructions * Update Code VM, memory and relay accounts * Update MemoryAccountWithData to use the new simple memory allocator * Small cleanup/consistency round * Use a new program key for deployment and testing * Remove mixed memory layout * Fix server * Updates from indexer changes --- go.mod | 2 +- go.sum | 4 +- .../async/sequencer/fulfillment_handler.go | 64 ----- pkg/code/async/sequencer/vm.go | 4 +- pkg/code/async/vm/storage.go | 19 +- pkg/code/common/account.go | 12 +- pkg/code/common/account_test.go | 6 +- pkg/code/data/treasury/treasury.go | 2 +- .../grpc/transaction/v2/action_handler.go | 77 +++--- .../server/grpc/transaction/v2/airdrop.go | 18 +- pkg/code/server/grpc/transaction/v2/errors.go | 2 +- pkg/code/server/grpc/transaction/v2/intent.go | 38 +-- pkg/code/transaction/transaction.go | 247 +++++------------- pkg/code/transaction/virtual_instruction.go | 134 ---------- pkg/solana/cvm/accounts_code_vm.go | 33 +-- pkg/solana/cvm/accounts_memory_account.go | 50 ++-- pkg/solana/cvm/accounts_relay.go | 52 ++-- pkg/solana/cvm/accounts_storage.go | 9 + pkg/solana/cvm/address.go | 118 ++++----- ...t_compress.go => instructions_compress.go} | 22 +- ...compress.go => instructions_decompress.go} | 26 +- ...ctions_vm_exec.go => instructions_exec.go} | 33 +-- ...ry_init.go => instructions_init_memory.go} | 33 ++- ...nce_init.go => instructions_init_nonce.go} | 22 +- ...lay_init.go => instructions_init_relay.go} | 38 ++- ...e_init.go => instructions_init_storage.go} | 32 +-- ..._init.go => instructions_init_timelock.go} | 22 +- ...ons_vm_init.go => instructions_init_vm.go} | 30 +-- ...esize.go => instructions_resize_memory.go} | 26 +- ...ecent_root.go => instructions_snapshot.go} | 20 +- ...da.go => instructions_timelock_deposit.go} | 22 +- .../instructions_timelock_deposit_from_ata.go | 92 ------- pkg/solana/cvm/program.go | 4 +- pkg/solana/cvm/types_account_type.go | 18 ++ pkg/solana/cvm/types_allocated_memory.go | 47 ---- pkg/solana/cvm/types_code_instruction.go | 31 +++ pkg/solana/cvm/types_hash.go | 6 +- pkg/solana/cvm/types_item_state.go | 49 ++++ pkg/solana/cvm/types_memory_allocator.go | 13 + pkg/solana/cvm/types_memory_layout.go | 21 +- pkg/solana/cvm/types_merkle_tree.go | 42 ++- pkg/solana/cvm/types_message.go | 56 ++++ pkg/solana/cvm/types_opcode.go | 17 +- pkg/solana/cvm/types_page.go | 64 ----- pkg/solana/cvm/types_paged_memory.go | 141 ---------- pkg/solana/cvm/types_recent_roots.go | 34 ++- pkg/solana/cvm/types_sector.go | 85 ------ .../cvm/types_simple_memory_allocator.go | 72 +++++ pkg/solana/cvm/utils.go | 4 + .../cvm/virtual_accounts_relay_account.go | 22 +- pkg/solana/cvm/virtual_instruction.go | 60 ----- pkg/solana/cvm/virtual_instructions.go | 6 + ...rtual_instructions_conditional_transfer.go | 25 ++ .../virtual_instructions_external_relay.go | 32 +++ .../virtual_instructions_external_transfer.go | 25 ++ .../virtual_instructions_external_withdraw.go | 22 ++ pkg/solana/cvm/virtual_instructions_relay.go | 32 +++ ...al_instructions_relay_transfer_external.go | 39 --- ...al_instructions_relay_transfer_internal.go | 39 --- ...instructions_timelock_transfer_external.go | 59 ----- ...instructions_timelock_transfer_internal.go | 59 ----- ...al_instructions_timelock_transfer_relay.go | 59 ----- ...instructions_timelock_withdraw_external.go | 87 ------ ...instructions_timelock_withdraw_internal.go | 87 ------ .../cvm/virtual_instructions_transfer.go | 25 ++ .../cvm/virtual_instructions_withdraw.go | 22 ++ 66 files changed, 876 insertions(+), 1837 deletions(-) delete mode 100644 pkg/code/transaction/virtual_instruction.go create mode 100644 pkg/solana/cvm/accounts_storage.go rename pkg/solana/cvm/{instructions_system_account_compress.go => instructions_compress.go} (61%) rename pkg/solana/cvm/{instructions_system_account_decompress.go => instructions_decompress.go} (66%) rename pkg/solana/cvm/{instructions_vm_exec.go => instructions_exec.go} (78%) rename pkg/solana/cvm/{instructions_vm_memory_init.go => instructions_init_memory.go} (59%) rename pkg/solana/cvm/{instructions_system_nonce_init.go => instructions_init_nonce.go} (62%) rename pkg/solana/cvm/{instructions_relay_init.go => instructions_init_relay.go} (62%) rename pkg/solana/cvm/{instructions_vm_storage_init.go => instructions_init_storage.go} (58%) rename pkg/solana/cvm/{instructions_system_timelock_init.go => instructions_init_timelock.go} (68%) rename pkg/solana/cvm/{instructions_vm_init.go => instructions_init_vm.go} (69%) rename pkg/solana/cvm/{instructions_vm_memory_resize.go => instructions_resize_memory.go} (60%) rename pkg/solana/cvm/{instructions_relay_save_recent_root.go => instructions_snapshot.go} (66%) rename pkg/solana/cvm/{instructions_timelock_deposit_from_pda.go => instructions_timelock_deposit.go} (70%) delete mode 100644 pkg/solana/cvm/instructions_timelock_deposit_from_ata.go create mode 100644 pkg/solana/cvm/types_account_type.go delete mode 100644 pkg/solana/cvm/types_allocated_memory.go create mode 100644 pkg/solana/cvm/types_code_instruction.go create mode 100644 pkg/solana/cvm/types_item_state.go create mode 100644 pkg/solana/cvm/types_memory_allocator.go create mode 100644 pkg/solana/cvm/types_message.go delete mode 100644 pkg/solana/cvm/types_page.go delete mode 100644 pkg/solana/cvm/types_paged_memory.go delete mode 100644 pkg/solana/cvm/types_sector.go create mode 100644 pkg/solana/cvm/types_simple_memory_allocator.go delete mode 100644 pkg/solana/cvm/virtual_instruction.go create mode 100644 pkg/solana/cvm/virtual_instructions.go create mode 100644 pkg/solana/cvm/virtual_instructions_conditional_transfer.go create mode 100644 pkg/solana/cvm/virtual_instructions_external_relay.go create mode 100644 pkg/solana/cvm/virtual_instructions_external_transfer.go create mode 100644 pkg/solana/cvm/virtual_instructions_external_withdraw.go create mode 100644 pkg/solana/cvm/virtual_instructions_relay.go delete mode 100644 pkg/solana/cvm/virtual_instructions_relay_transfer_external.go delete mode 100644 pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go delete mode 100644 pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go delete mode 100644 pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go delete mode 100644 pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go delete mode 100644 pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go delete mode 100644 pkg/solana/cvm/virtual_instructions_timelock_withdraw_internal.go create mode 100644 pkg/solana/cvm/virtual_instructions_transfer.go create mode 100644 pkg/solana/cvm/virtual_instructions_withdraw.go diff --git a/go.mod b/go.mod index dcd58720..ee78545e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886 - github.com/code-payments/code-vm-indexer v0.1.0 + github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba github.com/dghubble/oauth1 v0.7.3 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 diff --git a/go.sum b/go.sum index 9d0b0bcf..a167ee05 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886 h1:PLbMVgpFwwhI6Ji+izq/NnJQGq41OKQ2dP3oPpUEcCA= github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= -github.com/code-payments/code-vm-indexer v0.1.0 h1:XzBwFrZp1R+9POGF/zMy5o6/OCI2J+jGJ7qr4cL72rY= -github.com/code-payments/code-vm-indexer v0.1.0/go.mod h1:LtXqlb7ub0mPUNKlCPJbsEDQrkZvWTPSRM5hTdHcqpM= +github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba h1:Bkp+gmeb6Y2PWXfkSCTMBGWkb2P1BujRDSjWeI+0j5I= +github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba/go.mod h1:jSiifpiBpyBQ8q2R0MGEbkSgWC6sbdRTyDBntmW+j1E= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 85f9c545..d57595a7 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -291,11 +291,6 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti return nil, err } - virtualBlockhashBytes, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) - if err != nil { - return nil, err - } - actionRecord, err := h.data.GetActionById(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) if err != nil { return nil, err @@ -316,11 +311,6 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti return nil, err } - sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - destinationTokenAccount, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) if err != nil { return nil, err @@ -364,8 +354,6 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti selectedNonce.Blockhash, solana.Signature(virtualSignatureBytes), - virtualNonce, - solana.Blockhash(virtualBlockhashBytes), common.CodeVmAccount, nonceMemory, @@ -375,8 +363,6 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti destinationeMemory, destinationIndex, - sourceTimelockAccounts, - destinationTokenAccount, *actionRecord.Quantity, ) } else { @@ -385,8 +371,6 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti selectedNonce.Blockhash, solana.Signature(virtualSignatureBytes), - virtualNonce, - solana.Blockhash(virtualBlockhashBytes), common.CodeVmAccount, common.CodeVmOmnibusAccount, @@ -396,7 +380,6 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti sourceMemory, sourceIndex, - sourceTimelockAccounts, destinationTokenAccount, *actionRecord.Quantity, ) @@ -484,11 +467,6 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex return nil, err } - virtualBlockhashBytes, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) - if err != nil { - return nil, err - } - sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) if err != nil { return nil, err @@ -504,11 +482,6 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex return nil, err } - sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - destinationTokenAccount, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) if err != nil { return nil, err @@ -552,8 +525,6 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex selectedNonce.Blockhash, solana.Signature(virtualSignatureBytes), - virtualNonce, - solana.Blockhash(virtualBlockhashBytes), common.CodeVmAccount, nonceMemory, @@ -562,9 +533,6 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex sourceIndex, destinationeMemory, destinationIndex, - - sourceTimelockAccounts, - destinationTokenAccount, ) } else { txn, makeTxnErr = transaction_util.MakeExternalWithdrawTransaction( @@ -572,8 +540,6 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex selectedNonce.Blockhash, solana.Signature(virtualSignatureBytes), - virtualNonce, - solana.Blockhash(virtualBlockhashBytes), common.CodeVmAccount, common.CodeVmOmnibusAccount, @@ -583,7 +549,6 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx contex sourceMemory, sourceIndex, - sourceTimelockAccounts, destinationTokenAccount, ) } @@ -711,11 +676,6 @@ func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr return nil, err } - virtualBlockhashBytes, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) - if err != nil { - return nil, err - } - commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) if err != nil { return nil, err @@ -751,11 +711,6 @@ func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr return nil, err } - sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) if err != nil { return nil, err @@ -776,8 +731,6 @@ func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr selectedNonce.Blockhash, solana.Signature(virtualSignatureBytes), - virtualNonce, - solana.Blockhash(virtualBlockhashBytes), common.CodeVmAccount, common.CodeVmOmnibusAccount, @@ -789,10 +742,8 @@ func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr relayMemory, relayIndex, - sourceTimelockAccounts, treasuryPool, treasuryPoolVault, - commitmentVault, commitmentRecord.Amount, ) if err != nil { @@ -933,11 +884,6 @@ func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr return nil, err } - virtualBlockhashBytes, err := base58.Decode(*fulfillmentRecord.VirtualBlockhash) - if err != nil { - return nil, err - } - oldCommitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) if err != nil { return nil, err @@ -978,11 +924,6 @@ func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr return nil, err } - sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) if err != nil { return nil, err @@ -1003,8 +944,6 @@ func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr selectedNonce.Blockhash, solana.Signature(virtualSignatureBytes), - virtualNonce, - solana.Blockhash(virtualBlockhashBytes), common.CodeVmAccount, common.CodeVmOmnibusAccount, @@ -1016,10 +955,8 @@ func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr relayMemory, relayIndex, - sourceTimelockAccounts, treasuryPool, treasuryPoolVault, - commitmentVault, oldCommitmentRecord.Amount, ) if err != nil { @@ -1231,7 +1168,6 @@ func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx c treasuryPool, treasuryPoolVault, - destination, commitment, commitmentRecord.Amount, transcript, diff --git a/pkg/code/async/sequencer/vm.go b/pkg/code/async/sequencer/vm.go index dc4dd5c4..473ec02d 100644 --- a/pkg/code/async/sequencer/vm.go +++ b/pkg/code/async/sequencer/vm.go @@ -162,9 +162,7 @@ func getVirtualRelayAccountStateInMemory(ctx context.Context, vmIndexerClient in protoAccount := resp.Item.Account state := cvm.VirtualRelayAccount{ - Address: protoAccount.Address.Value, - Commitment: cvm.Hash(protoAccount.Commitment.Value), - RecentRoot: cvm.Hash(protoAccount.RecentRoot.Value), + Target: protoAccount.Target.Value, Destination: protoAccount.Destination.Value, } diff --git a/pkg/code/async/vm/storage.go b/pkg/code/async/vm/storage.go index e2e430a2..2aaba77a 100644 --- a/pkg/code/async/vm/storage.go +++ b/pkg/code/async/vm/storage.go @@ -21,8 +21,6 @@ import ( ) const ( - // todo: make these configurable - maxStorageAccountLevels = 20 // maximum for decompression under a single tx minStorageAccountCapacity = 50_000 // mimumum capacity for a storage until we decide we need a new one ) @@ -76,9 +74,8 @@ func (p *service) maybeInitStorageAccount(ctx context.Context) error { func (p *service) initStorageAccountOnBlockchain(ctx context.Context, vm *common.Account, purpose storage.Purpose) (*storage.Record, error) { name := fmt.Sprintf("storage-%d-%s", purpose, strings.Split(uuid.New().String(), "-")[0]) - levels := uint8(maxStorageAccountLevels) - address, _, err := cvm.GetStorageAccountAddress(&cvm.GetMemoryAccountAddressArgs{ + address, bump, err := cvm.GetStorageAccountAddress(&cvm.GetMemoryAccountAddressArgs{ Name: name, Vm: vm.PublicKey().ToBytes(), }) @@ -91,8 +88,8 @@ func (p *service) initStorageAccountOnBlockchain(ctx context.Context, vm *common Name: name, Address: base58.Encode(address), - Levels: levels, - AvailableCapacity: storage.GetMaxCapacity(levels), + Levels: cvm.DefaultCompressedStateDepth, + AvailableCapacity: storage.GetMaxCapacity(cvm.DefaultCompressedStateDepth), Purpose: purpose, } @@ -100,15 +97,15 @@ func (p *service) initStorageAccountOnBlockchain(ctx context.Context, vm *common common.GetSubsidizer().PublicKey().ToBytes(), compute_budget.SetComputeUnitLimit(100_000), compute_budget.SetComputeUnitPrice(10_000), - cvm.NewVmStorageInitInstruction( - &cvm.VmStorageInitInstructionAccounts{ + cvm.NewInitStorageInstruction( + &cvm.InitStorageInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmStorage: address, }, - &cvm.VmStorageInitInstructionArgs{ - Name: name, - Levels: levels, + &cvm.InitStorageInstructionArgs{ + Name: name, + VmStorageBump: bump, }, ), ) diff --git a/pkg/code/common/account.go b/pkg/code/common/account.go index 99bf6acb..faee4d97 100644 --- a/pkg/code/common/account.go +++ b/pkg/code/common/account.go @@ -209,9 +209,9 @@ func (a *Account) GetTimelockAccounts(vm, mint *Account) (*TimelockAccounts, err } unlockAddress, unlockBump, err := cvm.GetVmUnlockStateAccountAddress(&cvm.GetVmUnlockStateAccountAddressArgs{ - Owner: a.publicKey.ToBytes(), - VirtualTimelock: stateAddress, - Vm: vm.publicKey.ToBytes(), + VirtualAccountOwner: a.publicKey.ToBytes(), + VirtualAccount: stateAddress, + Vm: vm.publicKey.ToBytes(), }) if err != nil { return nil, errors.Wrap(err, "error getting unlock address") @@ -344,14 +344,14 @@ func (a *TimelockAccounts) GetDBRecord(ctx context.Context, data code_data.Provi // GetInitializeInstruction gets a SystemTimelockInitInstruction instruction for a timelock account func (a *TimelockAccounts) GetInitializeInstruction(memory *Account, accountIndex uint16) (solana.Instruction, error) { - return cvm.NewSystemTimelockInitInstruction( - &cvm.SystemTimelockInitInstructionAccounts{ + return cvm.NewInitTimelockInstruction( + &cvm.InitTimelockInstructionAccounts{ VmAuthority: GetSubsidizer().publicKey.ToBytes(), Vm: a.Vm.PublicKey().ToBytes(), VmMemory: memory.PublicKey().ToBytes(), VirtualAccountOwner: a.VaultOwner.PublicKey().ToBytes(), }, - &cvm.SystemTimelockInitInstructionArgs{ + &cvm.InitTimelockInstructionArgs{ AccountIndex: accountIndex, VirtualTimelockBump: a.StateBump, VirtualVaultBump: a.VaultBump, diff --git a/pkg/code/common/account_test.go b/pkg/code/common/account_test.go index f78c3906..5e5c6b3c 100644 --- a/pkg/code/common/account_test.go +++ b/pkg/code/common/account_test.go @@ -148,9 +148,9 @@ func TestGetTimelockAccounts(t *testing.T) { require.NoError(t, err) expectedUnlockAddress, expectedUnlockBump, err := cvm.GetVmUnlockStateAccountAddress(&cvm.GetVmUnlockStateAccountAddressArgs{ - Owner: ownerAccount.PublicKey().ToBytes(), - VirtualTimelock: expectedStateAddress, - Vm: vmAccount.PublicKey().ToBytes(), + VirtualAccountOwner: ownerAccount.PublicKey().ToBytes(), + VirtualAccount: expectedStateAddress, + Vm: vmAccount.PublicKey().ToBytes(), }) require.NoError(t, err) diff --git a/pkg/code/data/treasury/treasury.go b/pkg/code/data/treasury/treasury.go index eeb7dfca..165e2b49 100644 --- a/pkg/code/data/treasury/treasury.go +++ b/pkg/code/data/treasury/treasury.go @@ -99,7 +99,7 @@ func (r *Record) Update(data *cvm.RelayAccount, solanaBlock uint64) error { return errors.Wrap(err, "error decoding address") } - vaultAddressBytes, _, err := cvm.GetRelayVaultAddress(&cvm.GetRelayVaultAddressArgs{ + vaultAddressBytes, _, err := cvm.GetRelayDestinationAddress(&cvm.GetRelayDestinationAddressArgs{ RelayOrProof: addressBytes, }) if err != nil { diff --git a/pkg/code/server/grpc/transaction/v2/action_handler.go b/pkg/code/server/grpc/transaction/v2/action_handler.go index 2f366250..5d813b4e 100644 --- a/pkg/code/server/grpc/transaction/v2/action_handler.go +++ b/pkg/code/server/grpc/transaction/v2/action_handler.go @@ -20,7 +20,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/merkletree" "github.com/code-payments/code-server/pkg/code/data/timelock" - transaction_util "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/cvm" ) @@ -29,8 +28,8 @@ type newFulfillmentMetadata struct { // Signature metadata requiresClientSignature bool - expectedSigner *common.Account // Must be null if the requiresClientSignature is false - virtualIxnHash *cvm.Hash // Must be null if the requiresClientSignature is false + expectedSigner *common.Account // Must be null if the requiresClientSignature is false + virtualIxnHash *cvm.CompactMessage // Must be null if the requiresClientSignature is false // Additional metadata to add to the action and fulfillment record, which relates // specifically to the transaction or virtual instruction within the context of @@ -329,21 +328,17 @@ func (h *NoPrivacyTransferActionHandler) GetFulfillmentMetadata( ) (*newFulfillmentMetadata, error) { switch index { case 0: - virtualIxnHash, err := transaction_util.GetVirtualTransferWithAuthorityHash( - nonce, - bh, - h.source, - h.destination, - h.amount, - ) - if err != nil { - return nil, err - } + virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.destination.PublicKey().ToBytes(), + Amount: h.amount, + Nonce: cvm.Hash(bh), + }) return &newFulfillmentMetadata{ requiresClientSignature: true, expectedSigner: h.source.VaultOwner, - virtualIxnHash: virtualIxnHash, + virtualIxnHash: &virtualIxnHash, fulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, source: h.source.Vault, @@ -435,20 +430,16 @@ func (h *NoPrivacyWithdrawActionHandler) GetFulfillmentMetadata( ) (*newFulfillmentMetadata, error) { switch index { case 0: - virtualIxnHash, err := transaction_util.GetVirtualCloseAccountWithBalanceHash( - nonce, - bh, - h.source, - h.destination, - ) - if err != nil { - return nil, err - } + virtualIxnHash := cvm.GetCompactWithdrawMessage(&cvm.GetCompactWithdrawMessageArgs{ + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.destination.PublicKey().ToBytes(), + Nonce: cvm.Hash(bh), + }) return &newFulfillmentMetadata{ requiresClientSignature: true, expectedSigner: h.source.VaultOwner, - virtualIxnHash: virtualIxnHash, + virtualIxnHash: &virtualIxnHash, fulfillmentType: fulfillment.NoPrivacyWithdraw, source: h.source.Vault, @@ -600,7 +591,7 @@ func NewTemporaryPrivacyTransferActionHandler( return nil, err } - commitmentVaultAddress, _, err := cvm.GetRelayVaultAddress(&cvm.GetRelayVaultAddressArgs{ + commitmentVaultAddress, _, err := cvm.GetRelayDestinationAddress(&cvm.GetRelayDestinationAddressArgs{ RelayOrProof: proofAddress, }) if err != nil { @@ -696,21 +687,17 @@ func (h *TemporaryPrivacyTransferActionHandler) GetFulfillmentMetadata( disableActiveScheduling: h.isCollectedForHideInTheCrowdPrivacy, }, nil case 1: - virtualIxnHash, err := transaction_util.GetVirtualTransferWithAuthorityHash( - nonce, - bh, - h.source, - h.commitmentVault, - h.unsavedCommitmentRecord.Amount, - ) - if err != nil { - return nil, err - } + virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.commitmentVault.PublicKey().ToBytes(), + Amount: h.unsavedCommitmentRecord.Amount, + Nonce: cvm.Hash(bh), + }) return &newFulfillmentMetadata{ requiresClientSignature: true, expectedSigner: h.source.VaultOwner, - virtualIxnHash: virtualIxnHash, + virtualIxnHash: &virtualIxnHash, fulfillmentType: fulfillment.TemporaryPrivacyTransferWithAuthority, source: h.source.Vault, @@ -839,21 +826,17 @@ func (h *PermanentPrivacyUpgradeActionHandler) GetFulfillmentMetadata( nonce *common.Account, bh solana.Blockhash, ) (*newFulfillmentMetadata, error) { - virtualIxnHash, err := transaction_util.GetVirtualTransferWithAuthorityHash( - nonce, - bh, - h.source, - h.privacyUpgradeProof.newCommitmentVault, - h.commitmentBeingUpgraded.Amount, - ) - if err != nil { - return nil, err - } + virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.privacyUpgradeProof.newCommitmentVault.PublicKey().ToBytes(), + Amount: h.commitmentBeingUpgraded.Amount, + Nonce: cvm.Hash(bh), + }) return &newFulfillmentMetadata{ requiresClientSignature: true, expectedSigner: h.source.VaultOwner, - virtualIxnHash: virtualIxnHash, + virtualIxnHash: &virtualIxnHash, fulfillmentType: fulfillment.PermanentPrivacyTransferWithAuthority, source: h.source.Vault, diff --git a/pkg/code/server/grpc/transaction/v2/airdrop.go b/pkg/code/server/grpc/transaction/v2/airdrop.go index cb0d08aa..6592dc5f 100644 --- a/pkg/code/server/grpc/transaction/v2/airdrop.go +++ b/pkg/code/server/grpc/transaction/v2/airdrop.go @@ -33,6 +33,7 @@ import ( "github.com/code-payments/code-server/pkg/grpc/client" "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" + "github.com/code-payments/code-server/pkg/solana/cvm" ) // This is a quick and dirty file to get an initial airdrop feature out @@ -271,17 +272,12 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner selectedNonce.Unlock() }() - vixnHash, err := transaction.GetVirtualTransferWithAuthorityHash( - selectedNonce.Account, - selectedNonce.Blockhash, - s.airdropper, - destination, - quarkAmount, - ) - if err != nil { - log.WithError(err).Warn("failure making solana transaction") - return nil, err - } + vixnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ + Source: s.airdropper.Vault.PublicKey().ToBytes(), + Destination: destination.PublicKey().ToBytes(), + Amount: quarkAmount, + Nonce: cvm.Hash(selectedNonce.Blockhash), + }) virtualSig := ed25519.Sign(s.airdropper.VaultOwner.PrivateKey().ToBytes(), vixnHash[:]) intentRecord := &intent.Record{ diff --git a/pkg/code/server/grpc/transaction/v2/errors.go b/pkg/code/server/grpc/transaction/v2/errors.go index 354bcd19..c6e2aa56 100644 --- a/pkg/code/server/grpc/transaction/v2/errors.go +++ b/pkg/code/server/grpc/transaction/v2/errors.go @@ -202,7 +202,7 @@ func toInvalidTxnSignatureErrorDetails( func toInvalidVirtualIxnSignatureErrorDetails( actionId uint32, - virtualIxnHash cvm.Hash, + virtualIxnHash cvm.CompactMessage, signature *commonpb.Signature, ) *transactionpb.ErrorDetails { return &transactionpb.ErrorDetails{ diff --git a/pkg/code/server/grpc/transaction/v2/intent.go b/pkg/code/server/grpc/transaction/v2/intent.go index 4490c6ed..c569cd12 100644 --- a/pkg/code/server/grpc/transaction/v2/intent.go +++ b/pkg/code/server/grpc/transaction/v2/intent.go @@ -407,7 +407,7 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm requiresClientSignature bool expectedSigner *common.Account - virtualIxnHash *cvm.Hash + virtualIxnHash *cvm.CompactMessage } // Convert all actions into a set of fulfillments @@ -1413,32 +1413,12 @@ func toUpgradeableIntentProto(ctx context.Context, data code_data.Provider, inte } fulfillmentToUpgrade := fulfillmentRecords[0] - nonce, err := common.NewAccountFromPublicKeyString(*fulfillmentToUpgrade.VirtualNonce) - if err != nil { - return nil, err - } - - bh, err := base58.Decode(*fulfillmentToUpgrade.VirtualBlockhash) - if err != nil { - return nil, err - } - // todo: this can be heavily cached sourceAccountInfo, err := data.GetAccountInfoByTokenAddress(ctx, fulfillmentToUpgrade.Source) if err != nil { return nil, err } - sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfo.AuthorityAccount) - if err != nil { - return nil, err - } - - sourceTimelockAccounts, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - originalDestination, err := common.NewAccountFromPublicKeyString(commitmentRecord.Destination) if err != nil { return nil, err @@ -1454,27 +1434,13 @@ func toUpgradeableIntentProto(ctx context.Context, data code_data.Provider, inte return nil, err } - txn, err := transaction.GetVirtualTransferWithAuthorityTransaction( - nonce, - solana.Blockhash(bh), - - sourceTimelockAccounts, - originalDestination, - commitmentRecord.Amount, - ) - if err != nil { - return nil, err - } - clientSignature, err := base58.Decode(*fulfillmentToUpgrade.VirtualSignature) if err != nil { return nil, err } action := &transactionpb.UpgradeableIntent_UpgradeablePrivateAction{ - TransactionBlob: &commonpb.Transaction{ - Value: txn.Marshal(), - }, + TransactionBlob: nil, // todo: need a solution or can we get rid of this? ClientSignature: &commonpb.Signature{ Value: clientSignature, }, diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index ac6fd0c5..408e4cbd 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -71,14 +71,14 @@ func MakeCompressAccountTransaction( signature := ed25519.Sign(common.GetSubsidizer().PrivateKey().ToBytes(), hashedVirtualAccountState) - compressInstruction := cvm.NewSystemAccountCompressInstruction( - &cvm.SystemAccountCompressInstructionAccounts{ + compressInstruction := cvm.NewCompressInstruction( + &cvm.CompressInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemory: memory.PublicKey().ToBytes(), VmStorage: storage.PublicKey().ToBytes(), }, - &cvm.SystemAccountCompressInstructionArgs{ + &cvm.CompressInstructionArgs{ AccountIndex: accountIndex, Signature: cvm.Signature(signature), }, @@ -92,8 +92,6 @@ func MakeInternalWithdrawTransaction( bh solana.Blockhash, virtualSignature solana.Signature, - virtualNonce *common.Account, - virtualBlockhash solana.Blockhash, vm *common.Account, nonceMemory *common.Account, @@ -102,46 +100,25 @@ func MakeInternalWithdrawTransaction( sourceIndex uint16, destinationMemory *common.Account, destinationIndex uint16, - - source *common.TimelockAccounts, - destination *common.Account, ) (solana.Transaction, error) { mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) if err != nil { return solana.Transaction{}, err } - vixn := cvm.NewVirtualInstruction( - common.GetSubsidizer().PublicKey().ToBytes(), - &cvm.VirtualDurableNonce{ - Address: virtualNonce.PublicKey().ToBytes(), - Nonce: cvm.Hash(virtualBlockhash), - }, - cvm.NewTimelockWithdrawInternalVirtualInstructionCtor( - &cvm.TimelockWithdrawInternalVirtualInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - VirtualTimelock: source.State.PublicKey().ToBytes(), - VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), - Owner: source.VaultOwner.PublicKey().ToBytes(), - Destination: destination.PublicKey().ToBytes(), - Mint: source.Mint.PublicKey().ToBytes(), - }, - &cvm.TimelockWithdrawInternalVirtualInstructionArgs{ - TimelockBump: source.StateBump, - Signature: cvm.Signature(virtualSignature), - }, - ), - ) + vixn := cvm.NewWithdrawVirtualInstruction(&cvm.WithdrawVirtualInstructionArgs{ + Signature: cvm.Signature(virtualSignature), + }) - execInstruction := cvm.NewVmExecInstruction( - &cvm.VmExecInstructionAccounts{ + execInstruction := cvm.NewExecInstruction( + &cvm.ExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, VmMemC: mergedMemoryBanks.C, }, - &cvm.VmExecInstructionArgs{ + &cvm.ExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, MemBanks: mergedMemoryBanks.Indices, @@ -157,8 +134,6 @@ func MakeExternalWithdrawTransaction( bh solana.Blockhash, virtualSignature solana.Signature, - virtualNonce *common.Account, - virtualBlockhash solana.Blockhash, vm *common.Account, vmOmnibus *common.Account, @@ -168,8 +143,7 @@ func MakeExternalWithdrawTransaction( sourceMemory *common.Account, sourceIndex uint16, - source *common.TimelockAccounts, - destination *common.Account, + externalDestination *common.Account, ) (solana.Transaction, error) { mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory) if err != nil { @@ -178,40 +152,22 @@ func MakeExternalWithdrawTransaction( vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) - destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) + externalAddressPublicKeyBytes := ed25519.PublicKey(externalDestination.PublicKey().ToBytes()) - vixn := cvm.NewVirtualInstruction( - common.GetSubsidizer().PublicKey().ToBytes(), - &cvm.VirtualDurableNonce{ - Address: virtualNonce.PublicKey().ToBytes(), - Nonce: cvm.Hash(virtualBlockhash), - }, - cvm.NewTimelockWithdrawExternalVirtualInstructionCtor( - &cvm.TimelockWithdrawExternalVirtualInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - VirtualTimelock: source.State.PublicKey().ToBytes(), - VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), - Owner: source.VaultOwner.PublicKey().ToBytes(), - Destination: destination.PublicKey().ToBytes(), - Mint: source.Mint.PublicKey().ToBytes(), - }, - &cvm.TimelockWithdrawExternalVirtualInstructionArgs{ - TimelockBump: source.StateBump, - Signature: cvm.Signature(virtualSignature), - }, - ), - ) + vixn := cvm.NewWithdrawVirtualInstruction(&cvm.WithdrawVirtualInstructionArgs{ + Signature: cvm.Signature(virtualSignature), + }) - execInstruction := cvm.NewVmExecInstruction( - &cvm.VmExecInstructionAccounts{ + execInstruction := cvm.NewExecInstruction( + &cvm.ExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, VmOmnibus: &vmOmnibusPublicKeyBytes, - ExternalAddress: &destinationPublicKeyBytes, + ExternalAddress: &externalAddressPublicKeyBytes, }, - &cvm.VmExecInstructionArgs{ + &cvm.ExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex}, MemBanks: mergedMemoryBanks.Indices, @@ -227,8 +183,6 @@ func MakeInternalTransferWithAuthorityTransaction( bh solana.Blockhash, virtualSignature solana.Signature, - virtualNonce *common.Account, - virtualBlockhash solana.Blockhash, vm *common.Account, nonceMemory *common.Account, @@ -238,8 +192,6 @@ func MakeInternalTransferWithAuthorityTransaction( destinationMemory *common.Account, destinationIndex uint16, - source *common.TimelockAccounts, - destination *common.Account, kinAmountInQuarks uint64, ) (solana.Transaction, error) { mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) @@ -247,37 +199,20 @@ func MakeInternalTransferWithAuthorityTransaction( return solana.Transaction{}, err } - vixn := cvm.NewVirtualInstruction( - common.GetSubsidizer().PublicKey().ToBytes(), - &cvm.VirtualDurableNonce{ - Address: virtualNonce.PublicKey().ToBytes(), - Nonce: cvm.Hash(virtualBlockhash), - }, - cvm.NewTimelockTransferInternalVirtualInstructionCtor( - &cvm.TimelockTransferInternalVirtualInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - VirtualTimelock: source.State.PublicKey().ToBytes(), - VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), - Owner: source.VaultOwner.PublicKey().ToBytes(), - Destination: destination.PublicKey().ToBytes(), - }, - &cvm.TimelockTransferInternalVirtualInstructionArgs{ - TimelockBump: source.StateBump, - Amount: kinAmountInQuarks, - Signature: cvm.Signature(virtualSignature), - }, - ), - ) + vixn := cvm.NewTransferVirtualInstruction(&cvm.TransferVirtualInstructionArgs{ + Amount: kinAmountInQuarks, + Signature: cvm.Signature(virtualSignature), + }) - execInstruction := cvm.NewVmExecInstruction( - &cvm.VmExecInstructionAccounts{ + execInstruction := cvm.NewExecInstruction( + &cvm.ExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, VmMemC: mergedMemoryBanks.C, }, - &cvm.VmExecInstructionArgs{ + &cvm.ExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, destinationIndex}, MemBanks: mergedMemoryBanks.Indices, @@ -293,8 +228,6 @@ func MakeExternalTransferWithAuthorityTransaction( bh solana.Blockhash, virtualSignature solana.Signature, - virtualNonce *common.Account, - virtualBlockhash solana.Blockhash, vm *common.Account, vmOmnibus *common.Account, @@ -304,8 +237,7 @@ func MakeExternalTransferWithAuthorityTransaction( sourceMemory *common.Account, sourceIndex uint16, - source *common.TimelockAccounts, - destination *common.Account, + externalDestination *common.Account, kinAmountInQuarks uint64, ) (solana.Transaction, error) { mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory) @@ -313,42 +245,25 @@ func MakeExternalTransferWithAuthorityTransaction( return solana.Transaction{}, err } - destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) + externalAddressPublicKeyBytes := ed25519.PublicKey(externalDestination.PublicKey().ToBytes()) vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) - vixn := cvm.NewVirtualInstruction( - common.GetSubsidizer().PublicKey().ToBytes(), - &cvm.VirtualDurableNonce{ - Address: virtualNonce.PublicKey().ToBytes(), - Nonce: cvm.Hash(virtualBlockhash), - }, - cvm.NewTimelockTransferExternalVirtualInstructionCtor( - &cvm.TimelockTransferExternalVirtualInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - VirtualTimelock: source.State.PublicKey().ToBytes(), - VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), - Owner: source.VaultOwner.PublicKey().ToBytes(), - Destination: destination.PublicKey().ToBytes(), - }, - &cvm.TimelockTransferExternalVirtualInstructionArgs{ - TimelockBump: source.StateBump, - Amount: kinAmountInQuarks, - Signature: cvm.Signature(virtualSignature), - }, - ), - ) + vixn := cvm.NewExternalTransferVirtualInstruction(&cvm.TransferVirtualInstructionArgs{ + Amount: kinAmountInQuarks, + Signature: cvm.Signature(virtualSignature), + }) - execInstruction := cvm.NewVmExecInstruction( - &cvm.VmExecInstructionAccounts{ + execInstruction := cvm.NewExecInstruction( + &cvm.ExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, VmOmnibus: &vmOmnibusPublicKeyBytes, - ExternalAddress: &destinationPublicKeyBytes, + ExternalAddress: &externalAddressPublicKeyBytes, }, - &cvm.VmExecInstructionArgs{ + &cvm.ExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex}, MemBanks: mergedMemoryBanks.Indices, @@ -371,7 +286,6 @@ func MakeInternalTreasuryAdvanceTransaction( treasuryPool *common.Account, treasuryPoolVault *common.Account, - destination *common.Account, commitment *common.Account, kinAmountInQuarks uint64, transcript []byte, @@ -385,22 +299,15 @@ func MakeInternalTreasuryAdvanceTransaction( return solana.Transaction{}, err } - vixn := cvm.NewVirtualInstruction( - common.GetSubsidizer().PublicKey().ToBytes(), - nil, - cvm.NewRelayTransferInternalVirtualInstructionCtor( - &cvm.RelayTransferInternalVirtualInstructionAccounts{}, - &cvm.RelayTransferInternalVirtualInstructionArgs{ - Transcript: cvm.Hash(transcript), - RecentRoot: cvm.Hash(recentRoot), - Commitment: cvm.Hash(commitment.PublicKey().ToBytes()), - Amount: kinAmountInQuarks, - }, - ), - ) + vixn := cvm.NewRelayVirtualInstruction(&cvm.RelayVirtualInstructionArgs{ + Amount: kinAmountInQuarks, + Transcript: cvm.Hash(transcript), + RecentRoot: cvm.Hash(recentRoot), + Commitment: cvm.Hash(commitment.PublicKey().ToBytes()), + }) - execInstruction := cvm.NewVmExecInstruction( - &cvm.VmExecInstructionAccounts{ + execInstruction := cvm.NewExecInstruction( + &cvm.ExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, @@ -408,7 +315,7 @@ func MakeInternalTreasuryAdvanceTransaction( VmRelay: &treasuryPoolPublicKeyBytes, VmRelayVault: &treasuryPoolVaultPublicKeyBytes, }, - &cvm.VmExecInstructionArgs{ + &cvm.ExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{accountIndex, relayIndex}, MemBanks: mergedMemoryBanks.Indices, @@ -440,32 +347,25 @@ func MakeExternalTreasuryAdvanceTransaction( treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) - destinationPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) - - vixn := cvm.NewVirtualInstruction( - common.GetSubsidizer().PublicKey().ToBytes(), - nil, - cvm.NewRelayTransferExternalVirtualInstructionCtor( - &cvm.RelayTransferExternalVirtualInstructionAccounts{}, - &cvm.RelayTransferExternalVirtualInstructionArgs{ - Transcript: cvm.Hash(transcript), - RecentRoot: cvm.Hash(recentRoot), - Commitment: cvm.Hash(commitment.PublicKey().ToBytes()), - Amount: kinAmountInQuarks, - }, - ), - ) + externalAddressPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) + + vixn := cvm.NewExternalRelayVirtualInstruction(&cvm.ExternalRelayVirtualInstructionArgs{ + Amount: kinAmountInQuarks, + Transcript: cvm.Hash(transcript), + RecentRoot: cvm.Hash(recentRoot), + Commitment: cvm.Hash(commitment.PublicKey().ToBytes()), + }) - execInstruction := cvm.NewVmExecInstruction( - &cvm.VmExecInstructionAccounts{ + execInstruction := cvm.NewExecInstruction( + &cvm.ExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemA: &memoryAPublicKeyBytes, VmRelay: &treasuryPoolPublicKeyBytes, VmRelayVault: &treasuryPoolVaultPublicKeyBytes, - ExternalAddress: &destinationPublicKeyBytes, + ExternalAddress: &externalAddressPublicKeyBytes, }, - &cvm.VmExecInstructionArgs{ + &cvm.ExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{relayIndex}, MemBanks: []uint8{0}, @@ -481,8 +381,6 @@ func MakeCashChequeTransaction( bh solana.Blockhash, virtualSignature solana.Signature, - virtualNonce *common.Account, - virtualBlockhash solana.Blockhash, vm *common.Account, vmOmnibus *common.Account, @@ -494,10 +392,8 @@ func MakeCashChequeTransaction( relayMemory *common.Account, relayIndex uint16, - source *common.TimelockAccounts, treasuryPool *common.Account, treasuryPoolVault *common.Account, - commitmentVault *common.Account, kinAmountInQuarks uint64, ) (solana.Transaction, error) { vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) @@ -510,30 +406,13 @@ func MakeCashChequeTransaction( return solana.Transaction{}, err } - vixn := cvm.NewVirtualInstruction( - common.GetSubsidizer().PublicKey().ToBytes(), - &cvm.VirtualDurableNonce{ - Address: virtualNonce.PublicKey().ToBytes(), - Nonce: cvm.Hash(virtualBlockhash), - }, - cvm.NewTimelockTransferRelayVirtualInstructionCtor( - &cvm.TimelockTransferRelayVirtualInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - VirtualTimelock: source.State.PublicKey().ToBytes(), - VirtualTimelockVault: source.Vault.PublicKey().ToBytes(), - Owner: source.VaultOwner.PublicKey().ToBytes(), - RelayVault: commitmentVault.PublicKey().ToBytes(), - }, - &cvm.TimelockTransferRelayVirtualInstructionArgs{ - TimelockBump: source.StateBump, - Amount: kinAmountInQuarks, - Signature: cvm.Signature(virtualSignature), - }, - ), - ) + vixn := cvm.NewConditionalTransferVirtualInstruction(&cvm.ConditionalTransferVirtualInstructionArgs{ + Amount: kinAmountInQuarks, + Signature: cvm.Signature(virtualSignature), + }) - execInstruction := cvm.NewVmExecInstruction( - &cvm.VmExecInstructionAccounts{ + execInstruction := cvm.NewExecInstruction( + &cvm.ExecInstructionAccounts{ VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), Vm: vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, @@ -544,7 +423,7 @@ func MakeCashChequeTransaction( VmRelayVault: &treasuryPoolVaultPublicKeyBytes, ExternalAddress: &treasuryPoolVaultPublicKeyBytes, }, - &cvm.VmExecInstructionArgs{ + &cvm.ExecInstructionArgs{ Opcode: vixn.Opcode, MemIndices: []uint16{nonceIndex, sourceIndex, relayIndex}, MemBanks: mergedMemoryBanks.Indices, diff --git a/pkg/code/transaction/virtual_instruction.go b/pkg/code/transaction/virtual_instruction.go deleted file mode 100644 index 706e9792..00000000 --- a/pkg/code/transaction/virtual_instruction.go +++ /dev/null @@ -1,134 +0,0 @@ -package transaction - -import ( - "crypto/sha256" - - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/cvm" -) - -func GetVirtualTransferWithAuthorityHash( - nonce *common.Account, - bh solana.Blockhash, - - source *common.TimelockAccounts, - destination *common.Account, - kinAmountInQuarks uint64, -) (*cvm.Hash, error) { - txn, err := GetVirtualTransferWithAuthorityTransaction( - nonce, - bh, - source, - destination, - kinAmountInQuarks, - ) - if err != nil { - return nil, err - } - - hash := getVirtualTransactionHash(&txn) - return &hash, nil -} - -func GetVirtualCloseAccountWithBalanceHash( - nonce *common.Account, - bh solana.Blockhash, - - source *common.TimelockAccounts, - destination *common.Account, -) (*cvm.Hash, error) { - txn, err := GetVirtualWithdrawTransaction( - nonce, - bh, - source, - destination, - ) - if err != nil { - return nil, err - } - - hash := getVirtualTransactionHash(&txn) - return &hash, nil -} - -func GetVirtualTransferWithAuthorityTransaction( - nonce *common.Account, - bh solana.Blockhash, - - source *common.TimelockAccounts, - destination *common.Account, - kinAmountInQuarks uint64, -) (solana.Transaction, error) { - memoInstruction, err := MakeKreMemoInstruction() - if err != nil { - return solana.Transaction{}, err - } - - transferWithAuthorityInstruction, err := source.GetTransferWithAuthorityInstruction(destination, kinAmountInQuarks) - if err != nil { - return solana.Transaction{}, err - } - - instructions := []solana.Instruction{ - memoInstruction, - transferWithAuthorityInstruction, - } - txn, err := MakeNoncedTransaction(nonce, bh, instructions...) - if err != nil { - return solana.Transaction{}, err - } - return txn, nil -} - -func GetVirtualWithdrawTransaction( - nonce *common.Account, - bh solana.Blockhash, - - source *common.TimelockAccounts, - destination *common.Account, -) (solana.Transaction, error) { - memoInstruction, err := MakeKreMemoInstruction() - if err != nil { - return solana.Transaction{}, err - } - - revokeLockInstruction, err := source.GetRevokeLockWithAuthorityInstruction() - if err != nil { - return solana.Transaction{}, err - } - - deactivateLockInstruction, err := source.GetDeactivateInstruction() - if err != nil { - return solana.Transaction{}, err - } - - withdrawInstruction, err := source.GetWithdrawInstruction(destination) - if err != nil { - return solana.Transaction{}, err - } - - closeInstruction, err := source.GetCloseAccountsInstruction() - if err != nil { - return solana.Transaction{}, err - } - - instructions := []solana.Instruction{ - memoInstruction, - revokeLockInstruction, - deactivateLockInstruction, - withdrawInstruction, - closeInstruction, - } - txn, err := MakeNoncedTransaction(nonce, bh, instructions...) - if err != nil { - return solana.Transaction{}, err - } - return txn, nil -} - -func getVirtualTransactionHash(txn *solana.Transaction) cvm.Hash { - hasher := sha256.New() - hasher.Write(txn.Message.Marshal()) - return cvm.Hash(hasher.Sum(nil)) -} diff --git a/pkg/solana/cvm/accounts_code_vm.go b/pkg/solana/cvm/accounts_code_vm.go index 85807aa4..2a8e4d13 100644 --- a/pkg/solana/cvm/accounts_code_vm.go +++ b/pkg/solana/cvm/accounts_code_vm.go @@ -9,33 +9,31 @@ import ( ) const ( - // todo: func for real size - minCodeVmAccountSize = (8 + //discriminator + CodeVmAccountSize = (8 + //discriminator 32 + // authority 32 + // mint + 8 + // slot + HashSize + // poh TokenPoolSize + // omnibus 1 + // lock_duration 1 + // bump - 8 + // slot - HashSize + // poh - 7) // padding + 5) // padding ) -var CodeVmAccountDiscriminator = []byte{0xed, 0x82, 0x60, 0x0b, 0xbb, 0x2c, 0xc7, 0x55} +var CodeVmAccountDiscriminator = []byte{byte(AccountTypeCodeVm), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} type CodeVmAccount struct { Authority ed25519.PublicKey Mint ed25519.PublicKey + Slot uint64 + Poh Hash Omnibus TokenPool LockDuration uint8 Bump uint8 - Slot uint64 - Poh Hash - ChangeLog PagedMemory } func (obj *CodeVmAccount) Unmarshal(data []byte) error { - if len(data) < minCodeVmAccountSize { + if len(data) < CodeVmAccountSize { return ErrInvalidAccountData } @@ -49,28 +47,25 @@ func (obj *CodeVmAccount) Unmarshal(data []byte) error { getKey(data, &obj.Authority, &offset) getKey(data, &obj.Mint, &offset) + getUint64(data, &obj.Slot, &offset) + getHash(data, &obj.Poh, &offset) getTokenPool(data, &obj.Omnibus, &offset) getUint8(data, &obj.LockDuration, &offset) getUint8(data, &obj.Bump, &offset) - getUint64(data, &obj.Slot, &offset) - getHash(data, &obj.Poh, &offset) - offset += 5 - obj.ChangeLog = NewChangelogMemory() - getPagedMemory(data, &obj.ChangeLog, &offset) + offset += 5 // padding return nil } func (obj *CodeVmAccount) String() string { return fmt.Sprintf( - "CodeVmAccount{authority=%s,mint=%s,omnibus=%s,lock_duration=%d,bump=%d,slot=%d,poh=%s,changelog=%s}", + "CodeVmAccount{authority=%s,mint=%s,slot=%d,poh=%s,omnibus=%s,lock_duration=%d,bump=%d}", base58.Encode(obj.Authority), base58.Encode(obj.Mint), + obj.Slot, + obj.Poh.String(), obj.Omnibus.String(), obj.LockDuration, obj.Bump, - obj.Slot, - obj.Poh.String(), - obj.ChangeLog.String(), ) } diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index 50b0baa6..33029980 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -15,26 +15,27 @@ const ( type MemoryAccount struct { Vm ed25519.PublicKey - Bump uint8 Name string + Bump uint8 Layout MemoryLayout } type MemoryAccountWithData struct { Vm ed25519.PublicKey - Bump uint8 Name string + Bump uint8 Layout MemoryLayout - Data PagedMemory + Data SimpleMemoryAllocator // todo: support other implementations } const MemoryAccountSize = (8 + // discriminator 32 + // vm - 1 + // bump MaxMemoryAccountNameLength + // name - 1) // memory_layout + 1 + // bump + 1 + // layout + 6) // padding -var MemoryAccountDiscriminator = []byte{0x89, 0x7a, 0xdc, 0x6e, 0xdd, 0xca, 0x3e, 0x7f} +var MemoryAccountDiscriminator = []byte{byte(AccountTypeMemory), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} func (obj *MemoryAccount) Unmarshal(data []byte) error { if len(data) < MemoryAccountSize { @@ -50,9 +51,10 @@ func (obj *MemoryAccount) Unmarshal(data []byte) error { } getKey(data, &obj.Vm, &offset) - getUint8(data, &obj.Bump, &offset) getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) + getUint8(data, &obj.Bump, &offset) getMemoryLayout(data, &obj.Layout, &offset) + offset += 6 // padding return nil } @@ -81,32 +83,46 @@ func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { } getKey(data, &obj.Vm, &offset) - getUint8(data, &obj.Bump, &offset) getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) + getUint8(data, &obj.Bump, &offset) getMemoryLayout(data, &obj.Layout, &offset) + offset += 6 // padding + switch obj.Layout { - case MemoryLayoutMixed: - obj.Data = NewMixedAccountMemory() case MemoryLayoutTimelock: - obj.Data = NewTimelockAccountMemory() + capacity := CompactStateItems + itemSize := int(GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock)) + if len(data) < MemoryAccountSize+GetSimpleMemoryAllocatorSize(capacity, itemSize) { + return ErrInvalidAccountData + } + getSimpleMemoryAllocator(data, &obj.Data, capacity, itemSize, &offset) case MemoryLayoutNonce: - obj.Data = NewNonceAccountMemory() + capacity := CompactStateItems + itemSize := int(GetVirtualAccountSizeInMemory(VirtualDurableNonceSize)) + if len(data) < MemoryAccountSize+GetSimpleMemoryAllocatorSize(capacity, itemSize) { + return ErrInvalidAccountData + } + getSimpleMemoryAllocator(data, &obj.Data, capacity, itemSize, &offset) case MemoryLayoutRelay: - obj.Data = NewRelayAccountMemory() + capacity := CompactStateItems + itemSize := int(GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay)) + if len(data) < MemoryAccountSize+GetSimpleMemoryAllocatorSize(capacity, itemSize) { + return ErrInvalidAccountData + } + getSimpleMemoryAllocator(data, &obj.Data, capacity, itemSize, &offset) default: - return errors.New("unexpected memory layout") + return errors.New("unsupported memory layout") } - getPagedMemory(data, &obj.Data, &offset) return nil } func (obj *MemoryAccountWithData) String() string { return fmt.Sprintf( - "MemoryAccountWithData{vm=%s,bump=%d,name=%s,layout=%d,data=%s}", + "MemoryAccountWithData{vm=%s,name=%s,bump=%d,layout=%d,data=%s}", base58.Encode(obj.Vm), - obj.Bump, obj.Name, + obj.Bump, obj.Layout, obj.Data.String(), ) diff --git a/pkg/solana/cvm/accounts_relay.go b/pkg/solana/cvm/accounts_relay.go index 905573cc..de650f6f 100644 --- a/pkg/solana/cvm/accounts_relay.go +++ b/pkg/solana/cvm/accounts_relay.go @@ -9,36 +9,42 @@ import ( ) const ( + DefaultRelayStateDepth = 63 + DefaultRelayHistoryItems = 32 + MaxRelayAccountNameSize = 32 ) -const ( - minRelayAccountSize = (8 + //discriminator +var ( + RelayAccountSize = (8 + //discriminator 32 + // vm - 1 + // bump MaxRelayAccountNameSize + // name + TokenPoolSize + // treasury + 1 + // bump 1 + // num_levels 1 + // num_history - TokenPoolSize) // treasury + 4 + // padding + GetRecentRootsSize(DefaultRelayHistoryItems) + // recent_roots + GetMerkleTreeSize(DefaultRelayStateDepth)) // history ) -var RelayAccountDiscriminator = []byte{0xf2, 0xbb, 0xef, 0x5f, 0x89, 0xe1, 0xf5, 0x5c} +var RelayAccountDiscriminator = []byte{byte(AccountTypeRelay), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} type RelayAccount struct { Vm ed25519.PublicKey - Bump uint8 + Name string - Name string + Treasury TokenPool + Bump uint8 NumLevels uint8 NumHistory uint8 - Treasury TokenPool History MerkleTree RecentRoots RecentRoots } func (obj *RelayAccount) Unmarshal(data []byte) error { - if len(data) < minRelayAccountSize { + if len(data) < RelayAccountSize { return ErrInvalidAccountData } @@ -51,38 +57,28 @@ func (obj *RelayAccount) Unmarshal(data []byte) error { } getKey(data, &obj.Vm, &offset) + getFixedString(data, &obj.Name, MaxRelayAccountNameSize, &offset) + getTokenPool(data, &obj.Treasury, &offset) getUint8(data, &obj.Bump, &offset) - getString(data, &obj.Name, &offset) getUint8(data, &obj.NumLevels, &offset) getUint8(data, &obj.NumHistory, &offset) - - if len(data) < GetRelayAccountSize(int(obj.NumLevels), int(obj.NumHistory)) { - return ErrInvalidAccountData - } - - getTokenPool(data, &obj.Treasury, &offset) - getMerkleTree(data, &obj.History, &offset) - getRecentRoots(data, &obj.RecentRoots, &offset) + offset += 4 // padding + getRecentRoots(data, &obj.RecentRoots, DefaultRelayHistoryItems, &offset) + getMerkleTree(data, &obj.History, DefaultRelayStateDepth, &offset) return nil } func (obj *RelayAccount) String() string { return fmt.Sprintf( - "RelayAccount{vm=%s,bump=%d,name=%s,num_levels=%d,num_history=%d,treasury=%s,history=%s,recent_roots=%s}", + "RelayAccount{vm=%s,name=%s,treasury=%s,bump=%d,num_levels=%d,num_history=%d,recent_roots=%s,history=%s}", base58.Encode(obj.Vm), - obj.Bump, obj.Name, + obj.Treasury.String(), + obj.Bump, obj.NumLevels, obj.NumHistory, - obj.Treasury.String(), - obj.History.String(), obj.RecentRoots.String(), + obj.History.String(), ) } - -func GetRelayAccountSize(numLevels, numHistory int) int { - return (minRelayAccountSize + - +GetMerkleTreeSize(numLevels) + // history - +GetRecentRootsSize(numHistory)) // recent_roots -} diff --git a/pkg/solana/cvm/accounts_storage.go b/pkg/solana/cvm/accounts_storage.go new file mode 100644 index 00000000..3f62d38c --- /dev/null +++ b/pkg/solana/cvm/accounts_storage.go @@ -0,0 +1,9 @@ +package cvm + +const ( + DefaultCompressedStateDepth = 20 + + MaxStorageAccountNameSize = 32 +) + +// todo: Define account struct diff --git a/pkg/solana/cvm/address.go b/pkg/solana/cvm/address.go index bbb201a1..16bdac8f 100644 --- a/pkg/solana/cvm/address.go +++ b/pkg/solana/cvm/address.go @@ -2,7 +2,6 @@ package cvm import ( "crypto/ed25519" - "crypto/sha256" "encoding/binary" "github.com/code-payments/code-server/pkg/solana" @@ -13,19 +12,20 @@ const ( ) var ( - CodeVmPrefix = []byte("code-vm") - RelayCommitmentPrefix = []byte("relay_commitment") - TimelockStatePrefix = []byte("timelock_state") - TimelockVaultPrefix = []byte("timelock_vault") - VmMemoryAccountPrefix = []byte("vm_memory_account") - VmOmnibusPrefix = []byte("vm_omnibus") - VmDepositPdaPrefix = []byte("vm_deposit_pda") - VmProofAccountPrefix = []byte("vm_proof_account") - VmRelayAccountPrefix = []byte("vm_relay_account") - VmRelayVaultPrefix = []byte("vm_relay_vault") - VmStorageAccountPrefix = []byte("vm_storage_account") - VmUnlockPdaAccountPrefix = []byte("vm_unlock_pda_account") - VmWithdrawReceiptAccountPrefix = []byte("vm_withdraw_receipt_account") + CodeVmPrefix = []byte("code_vm") + VmOmnibusPrefix = []byte("vm_omnibus") + VmMemoryPrefix = []byte("vm_memory_account") + VmStoragePrefix = []byte("vm_storage_account") + VmDurableNoncePrefix = []byte("vm_durable_nonce") + VmUnlockPdaPrefix = []byte("vm_unlock_pda_account") + VmWithdrawReceiptPrefix = []byte("vm_withdraw_receipt_account") + VmDepositPdaPrefix = []byte("vm_deposit_pda") + VmRelayPrefix = []byte("vm_relay_account") + VmRelayProofPrefix = []byte("vm_proof_account") + VmRelayVaultPrefix = []byte("vm_relay_vault") + VmRelayCommitmentPrefix = []byte("relay_commitment") + VmTimelockStatePrefix = []byte("timelock_state") + VmTimelockVaultPrefix = []byte("timelock_vault") ) type GetVmAddressArgs struct { @@ -45,9 +45,7 @@ func GetVmAddress(args *GetVmAddressArgs) (ed25519.PublicKey, uint8, error) { } type GetVmObnibusAddressArgs struct { - Mint ed25519.PublicKey - VmAuthority ed25519.PublicKey - LockDuration uint8 + Vm ed25519.PublicKey } func GetVmObnibusAddress(args *GetVmObnibusAddressArgs) (ed25519.PublicKey, uint8, error) { @@ -55,9 +53,7 @@ func GetVmObnibusAddress(args *GetVmObnibusAddressArgs) (ed25519.PublicKey, uint PROGRAM_ID, CodeVmPrefix, VmOmnibusPrefix, - args.Mint, - args.VmAuthority, - []byte{args.LockDuration}, + args.Vm, ) } @@ -70,7 +66,7 @@ func GetMemoryAccountAddress(args *GetMemoryAccountAddressArgs) (ed25519.PublicK return solana.FindProgramAddressAndBump( PROGRAM_ID, CodeVmPrefix, - VmMemoryAccountPrefix, + VmMemoryPrefix, []byte(toFixedString(args.Name, MaxMemoryAccountNameLength)), args.Vm, ) @@ -85,8 +81,8 @@ func GetStorageAccountAddress(args *GetMemoryAccountAddressArgs) (ed25519.Public return solana.FindProgramAddressAndBump( PROGRAM_ID, CodeVmPrefix, - VmStorageAccountPrefix, - []byte(args.Name), + VmStoragePrefix, + []byte(toFixedString(args.Name, MaxStorageAccountNameSize)), args.Vm, ) } @@ -100,25 +96,12 @@ func GetRelayAccountAddress(args *GetRelayAccountAddressArgs) (ed25519.PublicKey return solana.FindProgramAddressAndBump( PROGRAM_ID, CodeVmPrefix, - VmRelayAccountPrefix, - []byte(args.Name), + VmRelayPrefix, + []byte(toFixedString(args.Name, MaxRelayAccountNameSize)), args.Vm, ) } -type GetRelayVaultAddressArgs struct { - RelayOrProof ed25519.PublicKey -} - -func GetRelayVaultAddress(args *GetRelayVaultAddressArgs) (ed25519.PublicKey, uint8, error) { - return solana.FindProgramAddressAndBump( - PROGRAM_ID, - CodeVmPrefix, - VmRelayVaultPrefix, - args.RelayOrProof, - ) -} - type GetRelayProofAddressArgs struct { Relay ed25519.PublicKey MerkleRoot Hash @@ -129,7 +112,7 @@ func GetRelayProofAddress(args *GetRelayProofAddressArgs) (ed25519.PublicKey, ui return solana.FindProgramAddressAndBump( PROGRAM_ID, CodeVmPrefix, - VmProofAccountPrefix, + VmRelayProofPrefix, args.Relay, args.MerkleRoot[:], args.Commitment[:], @@ -151,7 +134,7 @@ func GetRelayCommitmentAddress(args *GetRelayCommitmentAddressArgs) (ed25519.Pub return solana.FindProgramAddressAndBump( PROGRAM_ID, CodeVmPrefix, - RelayCommitmentPrefix, + VmRelayCommitmentPrefix, args.Relay, args.MerkleRoot[:], args.Transcript[:], @@ -160,19 +143,17 @@ func GetRelayCommitmentAddress(args *GetRelayCommitmentAddressArgs) (ed25519.Pub ) } -type GetVirtualDurableNonceAddressArgs struct { - Seed ed25519.PublicKey - Value Hash +type GetRelayDestinationAddressArgs struct { + RelayOrProof ed25519.PublicKey } -func GetVirtualDurableNonceAddress(args *GetVirtualDurableNonceAddressArgs) ed25519.PublicKey { - var combined [64]byte - copy(combined[0:32], args.Seed) - copy(combined[32:64], args.Value[:]) - - h := sha256.New() - h.Write(combined[:]) - return h.Sum(nil) +func GetRelayDestinationAddress(args *GetRelayDestinationAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + SPLITTER_PROGRAM_ID, + CodeVmPrefix, + VmRelayVaultPrefix, + args.RelayOrProof, + ) } type GetVirtualTimelockAccountAddressArgs struct { @@ -185,7 +166,7 @@ type GetVirtualTimelockAccountAddressArgs struct { func GetVirtualTimelockAccountAddress(args *GetVirtualTimelockAccountAddressArgs) (ed25519.PublicKey, uint8, error) { return solana.FindProgramAddressAndBump( TIMELOCK_PROGRAM_ID, - TimelockStatePrefix, + VmTimelockStatePrefix, args.Mint, args.VmAuthority, args.Owner, @@ -200,7 +181,7 @@ type GetVirtualTimelockVaultAddressArgs struct { func GetVirtualTimelockVaultAddress(args *GetVirtualTimelockVaultAddressArgs) (ed25519.PublicKey, uint8, error) { return solana.FindProgramAddressAndBump( TIMELOCK_PROGRAM_ID, - TimelockVaultPrefix, + VmTimelockVaultPrefix, args.VirtualTimelock, []byte{byte(TimelockDataVersion1)}, ) @@ -222,18 +203,18 @@ func GetVmDepositAddress(args *GetVmDepositAddressArgs) (ed25519.PublicKey, uint } type GetVmUnlockStateAccountAddressArgs struct { - Owner ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - Vm ed25519.PublicKey + VirtualAccountOwner ed25519.PublicKey + VirtualAccount ed25519.PublicKey + Vm ed25519.PublicKey } func GetVmUnlockStateAccountAddress(args *GetVmUnlockStateAccountAddressArgs) (ed25519.PublicKey, uint8, error) { return solana.FindProgramAddressAndBump( PROGRAM_ID, CodeVmPrefix, - VmUnlockPdaAccountPrefix, - args.Owner, - args.VirtualTimelock, + VmUnlockPdaPrefix, + args.VirtualAccountOwner, + args.VirtualAccount, args.Vm, ) } @@ -248,9 +229,26 @@ func GetWithdrawReceiptAccountAddress(args *GetWithdrawReceiptAccountAddressArgs return solana.FindProgramAddressAndBump( PROGRAM_ID, CodeVmPrefix, - VmWithdrawReceiptAccountPrefix, + VmWithdrawReceiptPrefix, args.UnlockAccount, args.Nonce[:], args.Vm, ) } + +type GetVirtualDurableNonceAddressArgs struct { + Seed ed25519.PublicKey + Poh Hash + Vm ed25519.PublicKey +} + +func GetVirtualDurableNonceAddress(args *GetVirtualDurableNonceAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + CodeVmPrefix, + VmDurableNoncePrefix, + args.Seed, + args.Poh[:], + args.Vm, + ) +} diff --git a/pkg/solana/cvm/instructions_system_account_compress.go b/pkg/solana/cvm/instructions_compress.go similarity index 61% rename from pkg/solana/cvm/instructions_system_account_compress.go rename to pkg/solana/cvm/instructions_compress.go index 7fad2a93..b3c14c3d 100644 --- a/pkg/solana/cvm/instructions_system_account_compress.go +++ b/pkg/solana/cvm/instructions_compress.go @@ -6,39 +6,33 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var SystemAccountCompressInstructionDiscriminator = []byte{ - 0x50, 0xc8, 0x0f, 0x6c, 0xb5, 0x1d, 0x7d, 0x9c, -} - const ( - SystemAccountCompressInstructionArgsSize = (2 + // account_index + CompressInstructionArgsSize = (2 + // account_index SignatureSize) // signature ) -type SystemAccountCompressInstructionArgs struct { +type CompressInstructionArgs struct { AccountIndex uint16 Signature Signature } -type SystemAccountCompressInstructionAccounts struct { +type CompressInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemory ed25519.PublicKey VmStorage ed25519.PublicKey } -func NewSystemAccountCompressInstruction( - accounts *SystemAccountCompressInstructionAccounts, - args *SystemAccountCompressInstructionArgs, +func NewCompressInstruction( + accounts *CompressInstructionAccounts, + args *CompressInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(SystemAccountCompressInstructionDiscriminator)+ - SystemAccountCompressInstructionArgsSize) + data := make([]byte, 1+CompressInstructionArgsSize) - putDiscriminator(data, SystemAccountCompressInstructionDiscriminator, &offset) + putCodeInstruction(data, CodeInstructionCompress, &offset) putUint16(data, args.AccountIndex, &offset) putSignature(data, args.Signature, &offset) diff --git a/pkg/solana/cvm/instructions_system_account_decompress.go b/pkg/solana/cvm/instructions_decompress.go similarity index 66% rename from pkg/solana/cvm/instructions_system_account_decompress.go rename to pkg/solana/cvm/instructions_decompress.go index c22c3dee..fe25db81 100644 --- a/pkg/solana/cvm/instructions_system_account_decompress.go +++ b/pkg/solana/cvm/instructions_decompress.go @@ -6,26 +6,22 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var SystemAccountDecompressInstructionDiscriminator = []byte{ - 0x63, 0x05, 0x07, 0x80, 0x20, 0x84, 0x86, 0x53, -} - const ( - MinSystemAccountDecompressInstructionArgsSize = (8 + // discriminator + MinDecompressInstructionArgsSize = (8 + // discriminator 2 + // account_index 4 + // len(packed_va) 4 + // len(proof) SignatureSize) // signature ) -type SystemAccountDecompressInstructionArgs struct { +type DecompressInstructionArgs struct { AccountIndex uint16 PackedVa []uint8 Proof HashArray Signature Signature } -type SystemAccountDecompressInstructionAccounts struct { +type DecompressInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemory ed25519.PublicKey @@ -34,18 +30,16 @@ type SystemAccountDecompressInstructionAccounts struct { WithdrawReceipt *ed25519.PublicKey } -func NewSystemAccountDecompressInstruction( - accounts *SystemAccountDecompressInstructionAccounts, - args *SystemAccountDecompressInstructionArgs, +func NewDecompressInstruction( + accounts *DecompressInstructionAccounts, + args *DecompressInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(SystemAccountDecompressInstructionDiscriminator)+ - GetSystemAccountDecompressInstructionArgsSize(args)) + data := make([]byte, 1+GetDecompressInstructionArgsSize(args)) - putDiscriminator(data, SystemAccountDecompressInstructionDiscriminator, &offset) + putCodeInstruction(data, CodeInstructionDecompress, &offset) putUint16(data, args.AccountIndex, &offset) putUint8Array(data, args.PackedVa, &offset) putHashArray(data, args.Proof, &offset) @@ -93,8 +87,8 @@ func NewSystemAccountDecompressInstruction( } } -func GetSystemAccountDecompressInstructionArgsSize(args *SystemAccountDecompressInstructionArgs) int { - return (MinSystemAccountDecompressInstructionArgsSize + +func GetDecompressInstructionArgsSize(args *DecompressInstructionArgs) int { + return (MinDecompressInstructionArgsSize + len(args.PackedVa) + // packed_va HashSize*len(args.Proof)) // proof } diff --git a/pkg/solana/cvm/instructions_vm_exec.go b/pkg/solana/cvm/instructions_exec.go similarity index 78% rename from pkg/solana/cvm/instructions_vm_exec.go rename to pkg/solana/cvm/instructions_exec.go index 4e6027c9..79fdeaa4 100644 --- a/pkg/solana/cvm/instructions_vm_exec.go +++ b/pkg/solana/cvm/instructions_exec.go @@ -6,23 +6,19 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var VmExecInstructionDiscriminator = []byte{ - 0xe5, 0xcf, 0x51, 0x74, 0xed, 0x96, 0xba, 0x3e, +type ExecArgsAndAccounts struct { + Args ExecInstructionArgs + Accounts ExecInstructionAccounts } -type VmExecArgsAndAccounts struct { - Args VmExecInstructionArgs - Accounts VmExecInstructionAccounts -} - -type VmExecInstructionArgs struct { +type ExecInstructionArgs struct { Opcode Opcode MemIndices []uint16 MemBanks []uint8 Data []uint8 } -type VmExecInstructionAccounts struct { +type ExecInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemA *ed25519.PublicKey @@ -35,18 +31,16 @@ type VmExecInstructionAccounts struct { ExternalAddress *ed25519.PublicKey } -func NewVmExecInstruction( - accounts *VmExecInstructionAccounts, - args *VmExecInstructionArgs, +func NewExecInstruction( + accounts *ExecInstructionAccounts, + args *ExecInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(VmExecInstructionDiscriminator)+ - getVmExecInstructionArgSize(args)) + data := make([]byte, 1+getExecInstructionArgSize(args)) - putDiscriminator(data, VmExecInstructionDiscriminator, &offset) + putCodeInstruction(data, CodeInstructionExec, &offset) putOpcode(data, args.Opcode, &offset) putUint16Array(data, args.MemIndices, &offset) putUint8Array(data, args.MemBanks, &offset) @@ -120,16 +114,11 @@ func NewVmExecInstruction( IsWritable: false, IsSigner: false, }, - { - PublicKey: SYSVAR_IXNS_PUBKEY, - IsWritable: false, - IsSigner: false, - }, }, } } -func getVmExecInstructionArgSize(args *VmExecInstructionArgs) int { +func getExecInstructionArgSize(args *ExecInstructionArgs) int { return (1 + // opcode 4 + 2*len(args.MemIndices) + // mem_indices 4 + len(args.MemBanks) + // mem_banks diff --git a/pkg/solana/cvm/instructions_vm_memory_init.go b/pkg/solana/cvm/instructions_init_memory.go similarity index 59% rename from pkg/solana/cvm/instructions_vm_memory_init.go rename to pkg/solana/cvm/instructions_init_memory.go index 39b12311..1257f6f7 100644 --- a/pkg/solana/cvm/instructions_vm_memory_init.go +++ b/pkg/solana/cvm/instructions_init_memory.go @@ -6,40 +6,37 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var VmMemoryInitInstructionDiscriminator = []byte{ - 0x05, 0xd3, 0xfb, 0x74, 0x39, 0xbc, 0xc1, 0xad, -} - const ( - VmMemoryInitInstructionArgsSize = (4 + MaxMemoryAccountNameLength + // name - 1) // layout + InitMemoryInstructionArgsSize = (4 + MaxMemoryAccountNameLength + // name + 1 + // layout + 1) // vm_memory_bump ) -type VmMemoryInitInstructionArgs struct { - Name string - Layout MemoryLayout +type InitMemoryInstructionArgs struct { + Name string + Layout MemoryLayout + VmMemoryBump uint8 } -type VmMemoryInitInstructionAccounts struct { +type InitMemoryInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemory ed25519.PublicKey } -func NewVmMemoryInitInstruction( - accounts *VmMemoryInitInstructionAccounts, - args *VmMemoryInitInstructionArgs, +func NewInitMemoryInstruction( + accounts *InitMemoryInstructionAccounts, + args *InitMemoryInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(VmMemoryInitInstructionDiscriminator)+ - VmMemoryInitInstructionArgsSize) + data := make([]byte, 1+InitMemoryInstructionArgsSize) - putDiscriminator(data, VmMemoryInitInstructionDiscriminator, &offset) - putString(data, args.Name, &offset) + putCodeInstruction(data, CodeInstructionInitMemory, &offset) + putFixedString(data, args.Name, MaxMemoryAccountNameLength, &offset) putMemoryLayout(data, args.Layout, &offset) + putUint8(data, args.VmMemoryBump, &offset) return solana.Instruction{ Program: PROGRAM_ADDRESS, diff --git a/pkg/solana/cvm/instructions_system_nonce_init.go b/pkg/solana/cvm/instructions_init_nonce.go similarity index 62% rename from pkg/solana/cvm/instructions_system_nonce_init.go rename to pkg/solana/cvm/instructions_init_nonce.go index bfe5b637..74ab450b 100644 --- a/pkg/solana/cvm/instructions_system_nonce_init.go +++ b/pkg/solana/cvm/instructions_init_nonce.go @@ -6,37 +6,31 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var SystemNonceInitInstructionDiscriminator = []byte{ - 0x0d, 0xb2, 0x98, 0x60, 0xa7, 0x2c, 0x96, 0x30, -} - const ( - SystemNonceInitInstructionArgsSize = 2 // account_index + InitNonceInstructionArgsSize = 2 // account_index ) -type SystemNonceInitInstructionArgs struct { +type InitNonceInstructionArgs struct { AccountIndex uint16 } -type SystemNonceInitInstructionAccounts struct { +type InitNonceInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemory ed25519.PublicKey VirtualAccountOwner ed25519.PublicKey } -func NewSystemNonceInitInstruction( - accounts *SystemNonceInitInstructionAccounts, - args *SystemNonceInitInstructionArgs, +func NewInitNonceInstruction( + accounts *InitNonceInstructionAccounts, + args *InitNonceInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(SystemNonceInitInstructionDiscriminator)+ - SystemNonceInitInstructionArgsSize) + data := make([]byte, 1+InitNonceInstructionArgsSize) - putDiscriminator(data, SystemNonceInitInstructionDiscriminator, &offset) + putCodeInstruction(data, CodeInstructionInitNonce, &offset) putUint16(data, args.AccountIndex, &offset) return solana.Instruction{ diff --git a/pkg/solana/cvm/instructions_relay_init.go b/pkg/solana/cvm/instructions_init_relay.go similarity index 62% rename from pkg/solana/cvm/instructions_relay_init.go rename to pkg/solana/cvm/instructions_init_relay.go index 9f022276..547ee45d 100644 --- a/pkg/solana/cvm/instructions_relay_init.go +++ b/pkg/solana/cvm/instructions_init_relay.go @@ -6,23 +6,19 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var RelayInitInstructionDiscriminator = []byte{ - 0x72, 0x6a, 0x94, 0xd1, 0x3c, 0x83, 0xb4, 0xe1, -} - const ( - RelayInitInstructionArgsSize = (1 + // num_levels - 1 + // num_history - MaxRelayAccountNameSize) // name + InitRelayInstructionArgsSize = (MaxRelayAccountNameSize + // name + 1 + // relay_bump + 1) // relay_vault_bump ) -type RelayInitInstructionArgs struct { - NumLevels uint8 - NumHistory uint8 - Name string +type InitRelayInstructionArgs struct { + Name string + RelayBump uint8 + RelayVaultBump uint8 } -type RelayInitInstructionAccounts struct { +type InitRelayInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey Relay ed25519.PublicKey @@ -30,21 +26,19 @@ type RelayInitInstructionAccounts struct { Mint ed25519.PublicKey } -func NewRelayInitInstruction( - accounts *RelayInitInstructionAccounts, - args *RelayInitInstructionArgs, +func NewInitRelayInstruction( + accounts *InitRelayInstructionAccounts, + args *InitRelayInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(RelayInitInstructionDiscriminator)+ - RelayInitInstructionArgsSize) + data := make([]byte, 1+InitRelayInstructionArgsSize) - putDiscriminator(data, RelayInitInstructionDiscriminator, &offset) - putUint8(data, args.NumLevels, &offset) - putUint8(data, args.NumHistory, &offset) - putString(data, args.Name, &offset) + putCodeInstruction(data, CodeInstructionInitRelay, &offset) + putFixedString(data, args.Name, MaxRelayAccountNameSize, &offset) + putUint8(data, args.RelayBump, &offset) + putUint8(data, args.RelayVaultBump, &offset) return solana.Instruction{ Program: PROGRAM_ADDRESS, diff --git a/pkg/solana/cvm/instructions_vm_storage_init.go b/pkg/solana/cvm/instructions_init_storage.go similarity index 58% rename from pkg/solana/cvm/instructions_vm_storage_init.go rename to pkg/solana/cvm/instructions_init_storage.go index d592808b..2318771c 100644 --- a/pkg/solana/cvm/instructions_vm_storage_init.go +++ b/pkg/solana/cvm/instructions_init_storage.go @@ -10,40 +10,34 @@ const ( MaxStorageAccountNameLength = 32 ) -var VmStorageInitInstructionDiscriminator = []byte{ - 0x9d, 0xdf, 0x16, 0xd1, 0x0f, 0x24, 0xfe, 0x09, -} - const ( - VmStorageInitInstructionArgsSize = (4 + MaxStorageAccountNameLength + // name - 1) // levels + InitStorageInstructionArgsSize = (MaxStorageAccountNameLength + // name + 1) // vm_storage_bump ) -type VmStorageInitInstructionArgs struct { - Name string - Levels uint8 +type InitStorageInstructionArgs struct { + Name string + VmStorageBump uint8 } -type VmStorageInitInstructionAccounts struct { +type InitStorageInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmStorage ed25519.PublicKey } -func NewVmStorageInitInstruction( - accounts *VmStorageInitInstructionAccounts, - args *VmStorageInitInstructionArgs, +func NewInitStorageInstruction( + accounts *InitStorageInstructionAccounts, + args *InitStorageInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(VmStorageInitInstructionDiscriminator)+ - VmStorageInitInstructionArgsSize) + data := make([]byte, 1+InitStorageInstructionArgsSize) - putDiscriminator(data, VmStorageInitInstructionDiscriminator, &offset) - putString(data, args.Name, &offset) - putUint8(data, args.Levels, &offset) + putCodeInstruction(data, CodeInstructionInitStorage, &offset) + putFixedString(data, args.Name, MaxStorageAccountNameLength, &offset) + putUint8(data, args.VmStorageBump, &offset) return solana.Instruction{ Program: PROGRAM_ADDRESS, diff --git a/pkg/solana/cvm/instructions_system_timelock_init.go b/pkg/solana/cvm/instructions_init_timelock.go similarity index 68% rename from pkg/solana/cvm/instructions_system_timelock_init.go rename to pkg/solana/cvm/instructions_init_timelock.go index fe491d48..c4596cc0 100644 --- a/pkg/solana/cvm/instructions_system_timelock_init.go +++ b/pkg/solana/cvm/instructions_init_timelock.go @@ -6,43 +6,37 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var SystemTimelockInitInstructionDiscriminator = []byte{ - 0x07, 0x0b, 0xf5, 0xc5, 0x68, 0xfe, 0xb7, 0xb6, -} - const ( - SystemTimelockInitInstructionArgsSize = (2 + // account_index + InitTimelockInstructionArgsSize = (2 + // account_index 1 + // virtual_timelock_bump 1 + // virtual_vault_bump 1) // vm_unlock_pda_bump ) -type SystemTimelockInitInstructionArgs struct { +type InitTimelockInstructionArgs struct { AccountIndex uint16 VirtualTimelockBump uint8 VirtualVaultBump uint8 VmUnlockPdaBump uint8 } -type SystemTimelockInitInstructionAccounts struct { +type InitTimelockInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemory ed25519.PublicKey VirtualAccountOwner ed25519.PublicKey } -func NewSystemTimelockInitInstruction( - accounts *SystemTimelockInitInstructionAccounts, - args *SystemTimelockInitInstructionArgs, +func NewInitTimelockInstruction( + accounts *InitTimelockInstructionAccounts, + args *InitTimelockInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(SystemTimelockInitInstructionDiscriminator)+ - SystemTimelockInitInstructionArgsSize) + data := make([]byte, 1+InitTimelockInstructionArgsSize) - putDiscriminator(data, SystemTimelockInitInstructionDiscriminator, &offset) + putCodeInstruction(data, CodeInstructionInitTimelock, &offset) putUint16(data, args.AccountIndex, &offset) putUint8(data, args.VirtualTimelockBump, &offset) putUint8(data, args.VirtualVaultBump, &offset) diff --git a/pkg/solana/cvm/instructions_vm_init.go b/pkg/solana/cvm/instructions_init_vm.go similarity index 69% rename from pkg/solana/cvm/instructions_vm_init.go rename to pkg/solana/cvm/instructions_init_vm.go index 2d14477d..43b67d10 100644 --- a/pkg/solana/cvm/instructions_vm_init.go +++ b/pkg/solana/cvm/instructions_init_vm.go @@ -6,38 +6,38 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var VmInitInstructionDiscriminator = []byte{ - 0xd3, 0xd4, 0x60, 0x15, 0xc2, 0x62, 0xe1, 0xfc, -} - const ( - VmInitInstructionArgsSize = 1 // lock_duration + InitVmInstructionArgsSize = (1 + // lock_duration + 1 + // vm_bump + 1) // vm_omnibus_bump ) -type VmInitInstructionArgs struct { - LockDuration uint8 +type InitVmInstructionArgs struct { + LockDuration uint8 + VmBump uint8 + VmOmnibusBump uint8 } -type VmInitInstructionAccounts struct { +type InitVmInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmOmnibus ed25519.PublicKey Mint ed25519.PublicKey } -func NewVmInitInstruction( - accounts *VmInitInstructionAccounts, - args *VmInitInstructionArgs, +func NewInitVmInstruction( + accounts *InitVmInstructionAccounts, + args *InitVmInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(VmInitInstructionDiscriminator)+ - VmInitInstructionArgsSize) + data := make([]byte, 1+InitVmInstructionArgsSize) - putDiscriminator(data, VmInitInstructionDiscriminator, &offset) + putCodeInstruction(data, CodeInstructionInitVm, &offset) putUint8(data, args.LockDuration, &offset) + putUint8(data, args.VmBump, &offset) + putUint8(data, args.VmOmnibusBump, &offset) return solana.Instruction{ Program: PROGRAM_ADDRESS, diff --git a/pkg/solana/cvm/instructions_vm_memory_resize.go b/pkg/solana/cvm/instructions_resize_memory.go similarity index 60% rename from pkg/solana/cvm/instructions_vm_memory_resize.go rename to pkg/solana/cvm/instructions_resize_memory.go index ad58dea1..bb559d1d 100644 --- a/pkg/solana/cvm/instructions_vm_memory_resize.go +++ b/pkg/solana/cvm/instructions_resize_memory.go @@ -6,37 +6,31 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var VmMemoryResizeInstructionDiscriminator = []byte{ - 0x6a, 0x40, 0x89, 0xc0, 0xdc, 0x48, 0xcd, 0x59, -} - const ( - VmMemoryResizeInstructionArgsSize = 4 // len + ResizeMemoryInstructionArgsSize = 4 // account_size ) -type VmMemoryResizeInstructionArgs struct { - Len uint32 +type ResizeMemoryInstructionArgs struct { + AccountSize uint32 } -type VmMemoryResizeInstructionAccounts struct { +type ResizeMemoryInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemory ed25519.PublicKey } -func NewVmMemoryResizeInstruction( - accounts *VmMemoryResizeInstructionAccounts, - args *VmMemoryResizeInstructionArgs, +func NewResizeMemoryInstruction( + accounts *ResizeMemoryInstructionAccounts, + args *ResizeMemoryInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(VmMemoryResizeInstructionDiscriminator)+ - VmMemoryResizeInstructionArgsSize) + data := make([]byte, 1+ResizeMemoryInstructionArgsSize) - putDiscriminator(data, VmMemoryResizeInstructionDiscriminator, &offset) - putUint32(data, args.Len, &offset) + putCodeInstruction(data, CodeInstructionResizeMemory, &offset) + putUint32(data, args.AccountSize, &offset) return solana.Instruction{ Program: PROGRAM_ADDRESS, diff --git a/pkg/solana/cvm/instructions_relay_save_recent_root.go b/pkg/solana/cvm/instructions_snapshot.go similarity index 66% rename from pkg/solana/cvm/instructions_relay_save_recent_root.go rename to pkg/solana/cvm/instructions_snapshot.go index 6801c826..eb52bb74 100644 --- a/pkg/solana/cvm/instructions_relay_save_recent_root.go +++ b/pkg/solana/cvm/instructions_snapshot.go @@ -6,10 +6,6 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var RelaySaveRecentRootInstructionDiscriminator = []byte{ - 0x9a, 0x54, 0x94, 0x43, 0x6e, 0xcc, 0x63, 0xcc, -} - const ( RelaySaveRecentRootInstructionArgsSize = 0 ) @@ -30,11 +26,9 @@ func NewRelaySaveRecentRootInstruction( var offset int // Serialize instruction arguments - data := make([]byte, - len(RelaySaveRecentRootInstructionDiscriminator)+ - RelaySaveRecentRootInstructionArgsSize) + data := make([]byte, 1+RelaySaveRecentRootInstructionArgsSize) - putDiscriminator(data, RelaySaveRecentRootInstructionDiscriminator, &offset) + putCodeInstruction(data, CodeInstructionSnapshot, &offset) return solana.Instruction{ Program: PROGRAM_ADDRESS, @@ -59,16 +53,6 @@ func NewRelaySaveRecentRootInstruction( IsWritable: true, IsSigner: false, }, - { - PublicKey: SYSTEM_PROGRAM_ID, - IsWritable: false, - IsSigner: false, - }, - { - PublicKey: SYSVAR_RENT_PUBKEY, - IsWritable: false, - IsSigner: false, - }, }, } } diff --git a/pkg/solana/cvm/instructions_timelock_deposit_from_pda.go b/pkg/solana/cvm/instructions_timelock_deposit.go similarity index 70% rename from pkg/solana/cvm/instructions_timelock_deposit_from_pda.go rename to pkg/solana/cvm/instructions_timelock_deposit.go index f63a8fcb..2eb3e454 100644 --- a/pkg/solana/cvm/instructions_timelock_deposit_from_pda.go +++ b/pkg/solana/cvm/instructions_timelock_deposit.go @@ -6,23 +6,19 @@ import ( "github.com/code-payments/code-server/pkg/solana" ) -var TimelockDepositFromPdaInstructionDiscriminator = []byte{ - 0x4c, 0xc5, 0xd9, 0x18, 0xb3, 0xe0, 0xdd, 0x9d, -} - const ( - TimelockDepositFromPdaInstructionArgsSize = (2 + // account_index + DepositInstructionArgsSize = (2 + // account_index 8 + //amount 1) // bump ) -type TimelockDepositFromPdaInstructionArgs struct { +type DepositInstructionArgs struct { AccountIndex uint16 Amount uint64 Bump uint8 } -type TimelockDepositFromPdaInstructionAccounts struct { +type DepositInstructionAccounts struct { VmAuthority ed25519.PublicKey Vm ed25519.PublicKey VmMemory ed25519.PublicKey @@ -32,18 +28,16 @@ type TimelockDepositFromPdaInstructionAccounts struct { VmOmnibus ed25519.PublicKey } -func NewTimelockDepositFromPdaInstruction( - accounts *TimelockDepositFromPdaInstructionAccounts, - args *TimelockDepositFromPdaInstructionArgs, +func NewDepositInstruction( + accounts *DepositInstructionAccounts, + args *DepositInstructionArgs, ) solana.Instruction { var offset int // Serialize instruction arguments - data := make([]byte, - len(TimelockDepositFromPdaInstructionDiscriminator)+ - TimelockDepositFromPdaInstructionArgsSize) + data := make([]byte, 1+DepositInstructionArgsSize) - putDiscriminator(data, TimelockDepositFromPdaInstructionDiscriminator, &offset) + putCodeInstruction(data, CodeInstructionDeposit, &offset) putUint16(data, args.AccountIndex, &offset) putUint64(data, args.Amount, &offset) putUint8(data, args.Bump, &offset) diff --git a/pkg/solana/cvm/instructions_timelock_deposit_from_ata.go b/pkg/solana/cvm/instructions_timelock_deposit_from_ata.go deleted file mode 100644 index 94bbe140..00000000 --- a/pkg/solana/cvm/instructions_timelock_deposit_from_ata.go +++ /dev/null @@ -1,92 +0,0 @@ -package cvm - -import ( - "crypto/ed25519" - - "github.com/code-payments/code-server/pkg/solana" -) - -var TimelockDepositFromAtaInstructionDiscriminator = []byte{ - 0xb8, 0x7a, 0x27, 0x63, 0x50, 0xc9, 0xa7, 0xd1, -} - -const ( - TimelockDepositFromAtaInstructionArgsSize = (2 + // account_index - 8) // amount -) - -type TimelockDepositFromAtaInstructionArgs struct { - AccountIndex uint16 - Amount uint64 -} - -type TimelockDepositFromAtaInstructionAccounts struct { - VmAuthority ed25519.PublicKey - Vm ed25519.PublicKey - VmMemory ed25519.PublicKey - VmOmnibus ed25519.PublicKey - DepositorAta ed25519.PublicKey - Depositor ed25519.PublicKey -} - -func NewTimelockDepositFromAtaInstruction( - accounts *TimelockDepositFromAtaInstructionAccounts, - args *TimelockDepositFromAtaInstructionArgs, -) solana.Instruction { - var offset int - - // Serialize instruction arguments - data := make([]byte, - len(TimelockDepositFromAtaInstructionDiscriminator)+ - TimelockDepositFromAtaInstructionArgsSize) - - putDiscriminator(data, TimelockDepositFromAtaInstructionDiscriminator, &offset) - putUint16(data, args.AccountIndex, &offset) - putUint64(data, args.Amount, &offset) - - return solana.Instruction{ - Program: PROGRAM_ADDRESS, - - // Instruction args - Data: data, - - // Instruction accounts - Accounts: []solana.AccountMeta{ - { - PublicKey: accounts.VmAuthority, - IsWritable: true, - IsSigner: true, - }, - { - PublicKey: accounts.Vm, - IsWritable: true, - IsSigner: false, - }, - { - PublicKey: accounts.VmMemory, - IsWritable: true, - IsSigner: false, - }, - { - PublicKey: accounts.VmOmnibus, - IsWritable: true, - IsSigner: false, - }, - { - PublicKey: accounts.DepositorAta, - IsWritable: true, - IsSigner: false, - }, - { - PublicKey: accounts.Depositor, - IsWritable: true, - IsSigner: true, - }, - { - PublicKey: SPL_TOKEN_PROGRAM_ID, - IsWritable: false, - IsSigner: false, - }, - }, - } -} diff --git a/pkg/solana/cvm/program.go b/pkg/solana/cvm/program.go index f585575d..94e7c15e 100644 --- a/pkg/solana/cvm/program.go +++ b/pkg/solana/cvm/program.go @@ -15,7 +15,7 @@ var ( var ( // todo: setup real program address - PROGRAM_ADDRESS = mustBase58Decode("vmTE1MUq7EBnZrXTLRRn2W9G2UMG6MEuh6UHngs3DuQ") + PROGRAM_ADDRESS = mustBase58Decode("vmT2hAx4N2U6DyjYxgQHER4VGC8tHJCfHNsSepBKCJZ") PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) ) @@ -23,7 +23,7 @@ var ( SYSTEM_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("11111111111111111111111111111111")) SPL_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) TIMELOCK_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("time2Z2SCnn3qYg3ULKVtdkh8YmZ5jFdKicnA1W2YnJ")) + SPLITTER_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("spLit2eb13Tz93if6aJM136nUWki5PVUsoEjcUjwpwW")) - SYSVAR_IXNS_PUBKEY = ed25519.PublicKey(mustBase58Decode("Sysvar1nstructions1111111111111111111111111")) SYSVAR_RENT_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarRent111111111111111111111111111111111")) ) diff --git a/pkg/solana/cvm/types_account_type.go b/pkg/solana/cvm/types_account_type.go new file mode 100644 index 00000000..6cdd5842 --- /dev/null +++ b/pkg/solana/cvm/types_account_type.go @@ -0,0 +1,18 @@ +package cvm + +type AccountType uint8 + +const ( + AccountTypeUnknown AccountType = iota + AccountTypeCodeVm + AccountTypeMemory + AccountTypeStorage + AccountTypeRelay + AccountTypeUnlockState + AccountTypeWithdrawReceipt +) + +func putAccountType(dst []byte, v AccountType, offset *int) { + dst[*offset] = uint8(v) + *offset += 1 +} diff --git a/pkg/solana/cvm/types_allocated_memory.go b/pkg/solana/cvm/types_allocated_memory.go deleted file mode 100644 index 07e107f5..00000000 --- a/pkg/solana/cvm/types_allocated_memory.go +++ /dev/null @@ -1,47 +0,0 @@ -package cvm - -import ( - "fmt" -) - -const AllocatedMemorySize = (2 + // size - 1 + // page - 1) // sector - -type AllocatedMemory struct { - Size uint16 - Page uint8 - Sector uint8 -} - -func (obj *AllocatedMemory) IsAllocated() bool { - return obj.Size != 0 -} - -func (obj *AllocatedMemory) Unmarshal(data []byte) error { - if len(data) < AllocatedMemorySize { - return ErrInvalidAccountData - } - - var offset int - - getUint16(data, &obj.Size, &offset) - getUint8(data, &obj.Page, &offset) - getUint8(data, &obj.Sector, &offset) - - return nil -} - -func (obj *AllocatedMemory) String() string { - return fmt.Sprintf( - "AllocatedMemory{size=%d,page=%d,sector=%d}", - obj.Size, - obj.Page, - obj.Sector, - ) -} - -func getAllocatedMemory(src []byte, dst *AllocatedMemory, offset *int) { - dst.Unmarshal(src[*offset:]) - *offset += AllocatedMemorySize -} diff --git a/pkg/solana/cvm/types_code_instruction.go b/pkg/solana/cvm/types_code_instruction.go new file mode 100644 index 00000000..7a16ee54 --- /dev/null +++ b/pkg/solana/cvm/types_code_instruction.go @@ -0,0 +1,31 @@ +package cvm + +type CodeInstruction uint8 + +const ( + Unknown CodeInstruction = iota + + CodeInstructionInitVm + CodeInstructionInitMemory + CodeInstructionInitStorage + CodeInstructionInitRelay + CodeInstructionInitNonce + CodeInstructionInitTimelock + CodeInstructionInitUnlock + + CodeInstructionExec + CodeInstructionCompress + CodeInstructionDecompress + + CodeInstructionResizeMemory + CodeInstructionSnapshot + + CodeInstructionDeposit + CodeInstructionWithdraw + CodeInstructionUnlockI +) + +func putCodeInstruction(dst []byte, v CodeInstruction, offset *int) { + dst[*offset] = uint8(v) + *offset += 1 +} diff --git a/pkg/solana/cvm/types_hash.go b/pkg/solana/cvm/types_hash.go index 15ac563b..1bafe7a1 100644 --- a/pkg/solana/cvm/types_hash.go +++ b/pkg/solana/cvm/types_hash.go @@ -26,15 +26,13 @@ func getHash(src []byte, dst *Hash, offset *int) { type HashArray []Hash -func getHashArray(src []byte, dst *HashArray, offset *int) { - length := binary.LittleEndian.Uint32(src[*offset:]) - *offset += 4 - +func getStaticHashArray(src []byte, dst *HashArray, length int, offset *int) { *dst = make([]Hash, length) for i := 0; i < int(length); i++ { getHash(src, &(*dst)[i], offset) } } + func putHashArray(dst []byte, v HashArray, offset *int) { binary.LittleEndian.PutUint32(dst[*offset:], uint32(len(v))) *offset += 4 diff --git a/pkg/solana/cvm/types_item_state.go b/pkg/solana/cvm/types_item_state.go new file mode 100644 index 00000000..7b98485b --- /dev/null +++ b/pkg/solana/cvm/types_item_state.go @@ -0,0 +1,49 @@ +package cvm + +import ( + "fmt" + "strings" +) + +type ItemState uint8 + +const ( + ItemStateEmpty ItemState = iota + ItemStateAllocated +) + +const ( + ItemStateSize = 1 +) + +func getItemState(src []byte, dst *ItemState, offset *int) { + *dst = ItemState(src[*offset]) + *offset += 1 +} + +func (obj ItemState) String() string { + switch obj { + case ItemStateEmpty: + return "empty" + case ItemStateAllocated: + return "allocated" + } + return "empty" +} + +type ItemStateArray []ItemState + +func getStaticItemStateArray(src []byte, dst *ItemStateArray, length int, offset *int) { + *dst = make([]ItemState, length) + for i := 0; i < int(length); i++ { + getItemState(src, &(*dst)[i], offset) + } +} + +func (obj ItemStateArray) String() string { + stringValues := make([]string, len(obj)) + for i := 0; i < len(obj); i++ { + stringValues[i] = obj[i].String() + } + return fmt.Sprintf("[%s]", strings.Join(stringValues, ",")) +} diff --git a/pkg/solana/cvm/types_memory_allocator.go b/pkg/solana/cvm/types_memory_allocator.go new file mode 100644 index 00000000..8b8f54bf --- /dev/null +++ b/pkg/solana/cvm/types_memory_allocator.go @@ -0,0 +1,13 @@ +package cvm + +const ( + CompactStateItems = 100 +) + +type MemoryAllocator interface { + IsAllocated(index int) bool + + Read(index int) ([]byte, bool) + + String() string +} diff --git a/pkg/solana/cvm/types_memory_layout.go b/pkg/solana/cvm/types_memory_layout.go index 92eb225b..c7a633ec 100644 --- a/pkg/solana/cvm/types_memory_layout.go +++ b/pkg/solana/cvm/types_memory_layout.go @@ -3,11 +3,7 @@ package cvm type MemoryLayout uint8 const ( - MixedMemoryLayoutPageSize = 32 -) - -const ( - MemoryLayoutMixed MemoryLayout = iota + MemoryLayoutUnknown MemoryLayout = iota MemoryLayoutTimelock MemoryLayoutNonce MemoryLayoutRelay @@ -21,18 +17,3 @@ func getMemoryLayout(src []byte, dst *MemoryLayout, offset *int) { *dst = MemoryLayout(src[*offset]) *offset += 1 } - -func GetPageSizeFromMemoryLayout(layout MemoryLayout) uint32 { - switch layout { - case MemoryLayoutMixed: - return MixedMemoryLayoutPageSize - case MemoryLayoutTimelock: - return GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock) - case MemoryLayoutNonce: - return GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce) - case MemoryLayoutRelay: - return GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay) - default: - return 0 - } -} diff --git a/pkg/solana/cvm/types_merkle_tree.go b/pkg/solana/cvm/types_merkle_tree.go index 41313ec7..7f51e1bc 100644 --- a/pkg/solana/cvm/types_merkle_tree.go +++ b/pkg/solana/cvm/types_merkle_tree.go @@ -3,7 +3,8 @@ package cvm import "fmt" const ( - MinMerkleTreeSize = 1 + minMerkleTreeSize = (HashSize + // root + 8) // next_index ) var ( @@ -11,55 +12,44 @@ var ( ) type MerkleTree struct { - Root Hash - Levels uint8 - NextIndex uint64 - + Root Hash FilledSubtrees HashArray ZeroValues HashArray + NextIndex uint64 } -func (obj *MerkleTree) Unmarshal(data []byte) error { - if len(data) < MinMerkleTreeSize { +func (obj *MerkleTree) Unmarshal(data []byte, levels int) error { + if len(data) < GetMerkleTreeSize(levels) { return ErrInvalidAccountData } var offset int getHash(data, &obj.Root, &offset) - getUint8(data, &obj.Levels, &offset) - - if len(data) < GetMerkleTreeSize(int(obj.Levels)) { - return ErrInvalidAccountData - } - + getStaticHashArray(data, &obj.FilledSubtrees, levels, &offset) + getStaticHashArray(data, &obj.ZeroValues, levels, &offset) getUint64(data, &obj.NextIndex, &offset) - getHashArray(data, &obj.FilledSubtrees, &offset) - getHashArray(data, &obj.ZeroValues, &offset) return nil } func (obj *MerkleTree) String() string { return fmt.Sprintf( - "MerkleTree{root=%s,levels=%d,next_index=%d,filled_subtrees=%s,zero_values=%s}", + "MerkleTree{root=%s,filled_subtrees=%s,zero_values=%s,next_index=%d}", obj.Root.String(), - obj.Levels, - obj.NextIndex, obj.FilledSubtrees.String(), obj.ZeroValues.String(), + obj.NextIndex, ) } -func getMerkleTree(src []byte, dst *MerkleTree, offset *int) { - dst.Unmarshal(src[*offset:]) - *offset += GetMerkleTreeSize(int(dst.Levels)) +func getMerkleTree(src []byte, dst *MerkleTree, levels int, offset *int) { + dst.Unmarshal(src[*offset:], levels) + *offset += GetMerkleTreeSize(int(levels)) } func GetMerkleTreeSize(levels int) int { - return (HashSize + // root - 1 + // levels - 8 + // next_index - 4 + levels*HashSize + // filled_subtrees - 4 + levels*HashSize) // zero_values + return (minMerkleTreeSize + + levels*HashSize + // filled_subtrees + levels*HashSize) // zero_values } diff --git a/pkg/solana/cvm/types_message.go b/pkg/solana/cvm/types_message.go new file mode 100644 index 00000000..07394b9f --- /dev/null +++ b/pkg/solana/cvm/types_message.go @@ -0,0 +1,56 @@ +package cvm + +import ( + "crypto/ed25519" + "crypto/sha256" + "encoding/binary" +) + +type Message []byte + +type CompactMessage Hash + +type GetCompactTransferMessageArgs struct { + Source ed25519.PublicKey + Destination ed25519.PublicKey + Amount uint64 + Nonce Hash +} + +func GetCompactTransferMessage(args *GetCompactTransferMessageArgs) CompactMessage { + amountBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(amountBytes, args.Amount) + + var message Message + message = append(message, []byte("transfer")...) + message = append(message, args.Source...) + message = append(message, args.Destination...) + message = append(message, amountBytes...) + message = append(message, args.Nonce[:]...) + return hashMessage(message) +} + +type GetCompactWithdrawMessageArgs struct { + Source ed25519.PublicKey + Destination ed25519.PublicKey + Nonce Hash +} + +func GetCompactWithdrawMessage(args *GetCompactWithdrawMessageArgs) CompactMessage { + var message Message + message = append(message, []byte("withdraw_and_close")...) + message = append(message, args.Source...) + message = append(message, args.Destination...) + + message = append(message, args.Nonce[:]...) + return hashMessage(message) +} + +func hashMessage(msg Message) CompactMessage { + h := sha256.New() + h.Write(msg) + bytes := h.Sum(nil) + var typed CompactMessage + copy(typed[:], bytes) + return typed +} diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index 5db22881..35401792 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -3,14 +3,17 @@ package cvm type Opcode uint8 const ( - OpcodeTimelockTransferToExternal Opcode = 10 - OpcodeTimelockTransferToInternal Opcode = 11 - OpcodeTimelockTransferToRelay Opcode = 12 - OpcodeTimelockWithdrawToExternal Opcode = 13 - OpcodeTimelockWithdrawToInternal Opcode = 14 + OpcodeUnknown Opcode = 0 - OpcodeSplitterTransferToExternal Opcode = 20 - OpcodeSplitterTransferToInternal Opcode = 21 + OpcodeTransfer Opcode = 11 + OpcodeWithdraw Opcode = 14 + OpcodeRelay Opcode = 21 + + OpcodeExternalTransfer Opcode = 10 + OpcodeExternalWithdraw Opcode = 13 + OpcodeExternalRelay Opcode = 20 + + OpcodeConditionalTransfer Opcode = 12 ) func putOpcode(dst []byte, v Opcode, offset *int) { diff --git a/pkg/solana/cvm/types_page.go b/pkg/solana/cvm/types_page.go deleted file mode 100644 index 0e32d188..00000000 --- a/pkg/solana/cvm/types_page.go +++ /dev/null @@ -1,64 +0,0 @@ -package cvm - -import ( - "errors" - "fmt" -) - -const ( - minPageSize = (1 + // is_allocated - 1) // NextPage -) - -type Page struct { - dataLen uint32 - - IsAllocated bool - Data []byte - NextPage uint8 -} - -func NewPage(dataLen uint32) Page { - return Page{ - dataLen: dataLen, - } -} - -func (obj *Page) Unmarshal(data []byte) error { - if obj.dataLen == 0 { - return errors.New("page not initialized") - } - - if len(data) < int(GetPageSize(int(obj.dataLen))) { - return ErrInvalidAccountData - } - - var offset int - - obj.Data = make([]byte, obj.dataLen) - - getBool(data, &obj.IsAllocated, &offset) - getBytes(data, obj.Data, int(obj.dataLen), &offset) - getUint8(data, &obj.NextPage, &offset) - - return nil -} - -func (obj *Page) String() string { - return fmt.Sprintf( - "Page{is_allocated=%v,data=%x,next_page=%d}", - obj.IsAllocated, - obj.Data, - obj.NextPage, - ) -} - -func getPage(src []byte, dst *Page, offset *int) { - dst.Unmarshal(src[*offset:]) - *offset += int(GetPageSize(int(dst.dataLen))) -} - -func GetPageSize(dataLen int) int { - return (minPageSize + - dataLen) // page_size -} diff --git a/pkg/solana/cvm/types_paged_memory.go b/pkg/solana/cvm/types_paged_memory.go deleted file mode 100644 index 7308a874..00000000 --- a/pkg/solana/cvm/types_paged_memory.go +++ /dev/null @@ -1,141 +0,0 @@ -package cvm - -import ( - "errors" - "fmt" - "strings" -) - -const ( - AccountMemoryCapacity = 100 // todo: set to 65536 - AccountMemorySectors = 2 // todo: set to 255 - AccountMemoryPages = 255 - MixedAccountMemoryPageSize = 32 - - ChangelogMemoryCapacity = 255 - ChangelogMemorySectors = 2 - ChangelogMemoryPages = 180 - ChangelogMemoryPageSize = 21 -) - -func NewAccountMemory(accountPageSize uint32) PagedMemory { - return PagedMemory{ - capacity: AccountMemoryCapacity, - numSectors: AccountMemorySectors, - numPages: AccountMemoryPages, - pageSize: accountPageSize, - } -} - -func NewTimelockAccountMemory() PagedMemory { - return NewAccountMemory(GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock)) -} - -func NewNonceAccountMemory() PagedMemory { - return NewAccountMemory(GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce)) -} - -func NewRelayAccountMemory() PagedMemory { - return NewAccountMemory(GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay)) -} - -func NewMixedAccountMemory() PagedMemory { - return NewAccountMemory(MixedAccountMemoryPageSize) -} - -func NewChangelogMemory() PagedMemory { - return PagedMemory{ - capacity: ChangelogMemoryCapacity, - numSectors: ChangelogMemorySectors, - numPages: ChangelogMemoryPages, - pageSize: ChangelogMemoryPageSize, - } -} - -type PagedMemory struct { - capacity uint32 - numSectors uint32 - numPages uint32 - pageSize uint32 - - Items []AllocatedMemory - Sectors []Sector -} - -func (obj *PagedMemory) Read(index int) ([]byte, bool) { - if index >= len(obj.Items) { - return nil, false - } - - account := obj.Items[index] - if !account.IsAllocated() { - return nil, false - } - - pages := obj.Sectors[account.Sector].GetLinkedPages(account.Page) - - var data []byte - for _, page := range pages { - if !page.IsAllocated { - return nil, false - } - - data = append(data, page.Data...) - } - - return data[:account.Size], true -} - -func (obj *PagedMemory) Unmarshal(data []byte) error { - if obj.capacity == 0 || obj.numSectors == 0 || obj.numPages == 0 || obj.pageSize == 0 { - return errors.New("paged memory not initialized") - } - - if len(data) < GetPagedMemorySize(int(obj.capacity), int(obj.numSectors), int(obj.numPages), int(obj.pageSize)) { - return ErrInvalidAccountData - } - - var offset int - - obj.Items = make([]AllocatedMemory, obj.capacity) - for i := 0; i < int(obj.numSectors); i++ { - obj.Sectors = append(obj.Sectors, NewSector(obj.numPages, obj.pageSize)) - } - - for i := 0; i < int(obj.capacity); i++ { - getAllocatedMemory(data, &obj.Items[i], &offset) - } - for i := 0; i < int(obj.numSectors); i++ { - getSector(data, &obj.Sectors[i], &offset) - } - - return nil -} - -func (obj *PagedMemory) String() string { - itemStrings := make([]string, len(obj.Items)) - for i, item := range obj.Items { - itemStrings[i] = item.String() - } - - sectorStrings := make([]string, len(obj.Sectors)) - for i, sector := range obj.Sectors { - sectorStrings[i] = sector.String() - } - - return fmt.Sprintf( - "PagedMemory{items=[%s],sectors=[%s]}", - strings.Join(itemStrings, ","), - strings.Join(sectorStrings, ","), - ) -} - -func getPagedMemory(src []byte, dst *PagedMemory, offset *int) { - dst.Unmarshal(src[*offset:]) - *offset += int(GetPagedMemorySize(int(dst.capacity), int(dst.numSectors), int(dst.numPages), int(dst.numPages))) -} - -func GetPagedMemorySize(capacity, numSectors, numPages, pageSize int) int { - return (capacity*AllocatedMemorySize + // accounts - numSectors*GetSectorSize(numPages, pageSize)) // sectors -} diff --git a/pkg/solana/cvm/types_recent_roots.go b/pkg/solana/cvm/types_recent_roots.go index 19b749b6..a73553fc 100644 --- a/pkg/solana/cvm/types_recent_roots.go +++ b/pkg/solana/cvm/types_recent_roots.go @@ -4,42 +4,48 @@ import ( "fmt" ) +const ( + minRecentRootsSize = (1 + // offset + 1 + // num_items + 6) // padding +) + type RecentRoots struct { - Capacity uint8 - Offset uint8 Items HashArray + Offset uint8 + NumItems uint8 } -func (obj *RecentRoots) Unmarshal(data []byte) error { +func (obj *RecentRoots) Unmarshal(data []byte, length int) error { if len(data) < 1 { return ErrInvalidAccountData } var offset int - getUint8(data, &obj.Capacity, &offset) + getStaticHashArray(data, &obj.Items, length, &offset) getUint8(data, &obj.Offset, &offset) - getHashArray(data, &obj.Items, &offset) + getUint8(data, &obj.NumItems, &offset) + offset += 6 // padding return nil } func (obj *RecentRoots) String() string { return fmt.Sprintf( - "RecentRoots{capacity=%d,offset=%d,items=%s}", - obj.Capacity, - obj.Offset, + "RecentRoots{items=%s,offset=%d,num_items=%d}", obj.Items.String(), + obj.Offset, + obj.NumItems, ) } -func getRecentRoots(src []byte, dst *RecentRoots, offset *int) { - dst.Unmarshal(src[*offset:]) +func getRecentRoots(src []byte, dst *RecentRoots, length int, offset *int) { + dst.Unmarshal(src[*offset:], length) *offset += GetRecentRootsSize(len(dst.Items)) } -func GetRecentRootsSize(numItems int) int { - return (1 + // capacity - 1 + // offset - 4 + numItems*HashSize) // items +func GetRecentRootsSize(length int) int { + return (minRecentRootsSize + + length*HashSize) // items } diff --git a/pkg/solana/cvm/types_sector.go b/pkg/solana/cvm/types_sector.go deleted file mode 100644 index 5bcc0f86..00000000 --- a/pkg/solana/cvm/types_sector.go +++ /dev/null @@ -1,85 +0,0 @@ -package cvm - -import ( - "errors" - "fmt" - "strings" -) - -const ( - minSectorSize = 1 // num_allocated -) - -type Sector struct { - numPages uint32 - pageSize uint32 - - NumAllocated uint8 - Pages []Page -} - -func NewSector(numPages uint32, pageSize uint32) Sector { - return Sector{ - numPages: numPages, - pageSize: pageSize, - } -} - -func (obj *Sector) GetLinkedPages(startIndex uint8) []Page { - var res []Page - current := startIndex - for { - res = append(res, obj.Pages[current]) - if obj.Pages[current].NextPage == 0 { - break - } - current = obj.Pages[current].NextPage - } - return res -} - -func (obj *Sector) Unmarshal(data []byte) error { - if obj.numPages == 0 || obj.pageSize == 0 { - return errors.New("sector not initialized") - } - - if len(data) < GetSectorSize(int(obj.numPages), int(obj.pageSize)) { - return ErrInvalidAccountData - } - - var offset int - - for i := 0; i < int(obj.numPages); i++ { - obj.Pages = append(obj.Pages, NewPage(obj.pageSize)) - } - - getUint8(data, &obj.NumAllocated, &offset) - for i := 0; i < int(obj.numPages); i++ { - getPage(data, &obj.Pages[i], &offset) - } - - return nil -} - -func (obj *Sector) String() string { - pageStrings := make([]string, len(obj.Pages)) - for i, page := range obj.Pages { - pageStrings[i] = page.String() - } - - return fmt.Sprintf( - "Sector{num_allocated=%d,pages=[%s]}", - obj.NumAllocated, - strings.Join(pageStrings, ","), - ) -} - -func getSector(src []byte, dst *Sector, offset *int) { - dst.Unmarshal(src[*offset:]) - *offset += int(GetSectorSize(int(dst.numPages), int(dst.pageSize))) -} - -func GetSectorSize(numPages, pageSize int) int { - return (minSectorSize + - numPages*pageSize) // pages -} diff --git a/pkg/solana/cvm/types_simple_memory_allocator.go b/pkg/solana/cvm/types_simple_memory_allocator.go new file mode 100644 index 00000000..d4fcd271 --- /dev/null +++ b/pkg/solana/cvm/types_simple_memory_allocator.go @@ -0,0 +1,72 @@ +package cvm + +import ( + "fmt" + "strings" + + "github.com/mr-tron/base58" +) + +type SimpleMemoryAllocator struct { + State ItemStateArray + Data [][]byte +} + +func (obj *SimpleMemoryAllocator) Unmarshal(data []byte, capacity, itemSize int) error { + if len(data) < GetSimpleMemoryAllocatorSize(capacity, itemSize) { + return ErrInvalidAccountData + } + + var offset int + getStaticItemStateArray(data, &obj.State, capacity, &offset) + + obj.Data = make([][]byte, capacity) + for i := 0; i < capacity; i++ { + obj.Data[i] = make([]byte, itemSize) + copy(obj.Data[i], data[offset:offset+itemSize]) + offset += itemSize + } + + return nil +} + +func getSimpleMemoryAllocator(src []byte, dst *SimpleMemoryAllocator, capacity, itemSize int, offset *int) { + dst.Unmarshal(src[*offset:], capacity, itemSize) + *offset += GetSimpleMemoryAllocatorSize(capacity, itemSize) +} + +func (obj *SimpleMemoryAllocator) IsAllocated(index int) bool { + if index >= len(obj.State) { + return false + } + return obj.State[index] == ItemStateAllocated +} + +func (obj *SimpleMemoryAllocator) Read(index int) ([]byte, bool) { + if !obj.IsAllocated(index) { + return nil, false + } + + copied := make([]byte, len(obj.Data[index])) + copy(copied, obj.Data[index]) + return copied, true +} + +func (obj *SimpleMemoryAllocator) String() string { + dataStringValues := make([]string, len(obj.Data)) + for i := 0; i < len(obj.Data); i++ { + dataStringValues[i] = base58.Encode(obj.Data[i]) + } + dataString := fmt.Sprintf("[%s]", strings.Join(dataStringValues, ",")) + + return fmt.Sprintf( + "SimpleMemoryAllocator{state=%s,data=%s}", + obj.State.String(), + dataString, + ) +} + +func GetSimpleMemoryAllocatorSize(capacity, itemSize int) int { + return (capacity*ItemStateSize + // state + capacity*itemSize) // data +} diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go index 49fbdd0e..7ace1e53 100644 --- a/pkg/solana/cvm/utils.go +++ b/pkg/solana/cvm/utils.go @@ -50,6 +50,10 @@ func getString(data []byte, dst *string, offset *int) { *offset += length } +func putFixedString(dst []byte, v string, length int, offset *int) { + copy(dst[*offset:], toFixedString(v, length)) + *offset += length +} func getFixedString(data []byte, dst *string, length int, offset *int) { *dst = string(data[*offset : *offset+length]) *dst = removeFixedStringPadding(*dst) diff --git a/pkg/solana/cvm/virtual_accounts_relay_account.go b/pkg/solana/cvm/virtual_accounts_relay_account.go index 51e1ebb4..281fd7e7 100644 --- a/pkg/solana/cvm/virtual_accounts_relay_account.go +++ b/pkg/solana/cvm/virtual_accounts_relay_account.go @@ -7,15 +7,11 @@ import ( "github.com/mr-tron/base58" ) -const VirtualRelayAccountSize = (32 + // address - 32 + // commitment - 32 + // recent_root +const VirtualRelayAccountSize = (32 + // target 32) // destination type VirtualRelayAccount struct { - Address ed25519.PublicKey - Commitment Hash - RecentRoot Hash + Target ed25519.PublicKey Destination ed25519.PublicKey } @@ -24,9 +20,7 @@ func (obj *VirtualRelayAccount) Marshal() []byte { var offset int - putKey(data, obj.Address, &offset) - putHash(data, obj.Commitment, &offset) - putHash(data, obj.RecentRoot, &offset) + putKey(data, obj.Target, &offset) putKey(data, obj.Destination, &offset) return data @@ -39,9 +33,7 @@ func (obj *VirtualRelayAccount) UnmarshalDirectly(data []byte) error { var offset int - getKey(data, &obj.Address, &offset) - getHash(data, &obj.Commitment, &offset) - getHash(data, &obj.RecentRoot, &offset) + getKey(data, &obj.Target, &offset) getKey(data, &obj.Destination, &offset) return nil @@ -61,10 +53,8 @@ func (obj *VirtualRelayAccount) UnmarshalFromMemory(data []byte) error { func (obj *VirtualRelayAccount) String() string { return fmt.Sprintf( - "VirtualRelayAccount{address=%s,commitment=%s,recent_root=%s,destination=%s}", - base58.Encode(obj.Address), - obj.Commitment.String(), - obj.RecentRoot.String(), + "VirtualRelayAccount{target=%s,destination=%s}", + base58.Encode(obj.Target), base58.Encode(obj.Destination), ) } diff --git a/pkg/solana/cvm/virtual_instruction.go b/pkg/solana/cvm/virtual_instruction.go deleted file mode 100644 index d051f2d8..00000000 --- a/pkg/solana/cvm/virtual_instruction.go +++ /dev/null @@ -1,60 +0,0 @@ -package cvm - -import ( - "crypto/ed25519" - "crypto/sha256" - - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/memo" - "github.com/code-payments/code-server/pkg/solana/system" -) - -// VirtualInstruction represents a virtual transaction instruction within the VM -type VirtualInstruction struct { - Opcode Opcode - Data []byte - Hash *Hash // Provided when user signature is required -} - -type VirtualInstructionCtor func() (Opcode, []solana.Instruction, []byte) - -func NewVirtualInstruction( - vmAuthority ed25519.PublicKey, - nonce *VirtualDurableNonce, - vixnCtor VirtualInstructionCtor, -) VirtualInstruction { - opcode, ixns, data := vixnCtor() - - var hash *Hash - if len(ixns) > 0 { - ixns = append([]solana.Instruction{system.AdvanceNonce(nonce.Address, vmAuthority)}, ixns...) - txn := solana.NewTransaction( - vmAuthority, - ixns..., - ) - txn.SetBlockhash(solana.Blockhash(nonce.Nonce)) - - txnHash := getTxnMessageHash(txn) - hash = &txnHash - } - - return VirtualInstruction{ - Opcode: opcode, - Data: data, - Hash: hash, - } -} - -func getTxnMessageHash(txn solana.Transaction) Hash { - msg := txn.Message.Marshal() - h := sha256.New() - h.Write(msg) - bytes := h.Sum(nil) - var typed Hash - copy(typed[:], bytes) - return typed -} - -func newKreMemoIxn() solana.Instruction { - return memo.Instruction("ZTAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") -} diff --git a/pkg/solana/cvm/virtual_instructions.go b/pkg/solana/cvm/virtual_instructions.go new file mode 100644 index 00000000..0c5c303d --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions.go @@ -0,0 +1,6 @@ +package cvm + +type VirtualInstruction struct { + Opcode Opcode + Data []byte +} diff --git a/pkg/solana/cvm/virtual_instructions_conditional_transfer.go b/pkg/solana/cvm/virtual_instructions_conditional_transfer.go new file mode 100644 index 00000000..e7f47e82 --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_conditional_transfer.go @@ -0,0 +1,25 @@ +package cvm + +const ( + ConditionalTransferVirtrualInstructionDataSize = (SignatureSize + // signature + 8) // amount +) + +type ConditionalTransferVirtualInstructionArgs struct { + Amount uint64 + Signature Signature +} + +func NewConditionalTransferVirtualInstruction( + args *ConditionalTransferVirtualInstructionArgs, +) VirtualInstruction { + var offset int + data := make([]byte, ConditionalTransferVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + putUint64(data, args.Amount, &offset) + + return VirtualInstruction{ + Opcode: OpcodeConditionalTransfer, + Data: data, + } +} diff --git a/pkg/solana/cvm/virtual_instructions_external_relay.go b/pkg/solana/cvm/virtual_instructions_external_relay.go new file mode 100644 index 00000000..07d0d386 --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_external_relay.go @@ -0,0 +1,32 @@ +package cvm + +const ( + ExternalRelayVirtrualInstructionDataSize = (8 + // amount + HashSize + // transcript + HashSize + // recent_root + HashSize) // commitment +) + +type ExternalRelayVirtualInstructionArgs struct { + Amount uint64 + Transcript Hash + RecentRoot Hash + Commitment Hash +} + +func NewExternalRelayVirtualInstruction( + args *ExternalRelayVirtualInstructionArgs, +) VirtualInstruction { + var offset int + data := make([]byte, ExternalRelayVirtrualInstructionDataSize) + + putUint64(data, args.Amount, &offset) + putHash(data, args.Transcript, &offset) + putHash(data, args.RecentRoot, &offset) + putHash(data, args.Commitment, &offset) + + return VirtualInstruction{ + Opcode: OpcodeExternalRelay, + Data: data, + } +} diff --git a/pkg/solana/cvm/virtual_instructions_external_transfer.go b/pkg/solana/cvm/virtual_instructions_external_transfer.go new file mode 100644 index 00000000..29cd572d --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_external_transfer.go @@ -0,0 +1,25 @@ +package cvm + +const ( + ExternalTransferVirtrualInstructionDataSize = (SignatureSize + // signature + 8) // amount +) + +type ExternalTransferVirtualInstructionArgs struct { + Amount uint64 + Signature Signature +} + +func NewExternalTransferVirtualInstruction( + args *TransferVirtualInstructionArgs, +) VirtualInstruction { + var offset int + data := make([]byte, ExternalTransferVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + putUint64(data, args.Amount, &offset) + + return VirtualInstruction{ + Opcode: OpcodeExternalTransfer, + Data: data, + } +} diff --git a/pkg/solana/cvm/virtual_instructions_external_withdraw.go b/pkg/solana/cvm/virtual_instructions_external_withdraw.go new file mode 100644 index 00000000..5a4b98a9 --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_external_withdraw.go @@ -0,0 +1,22 @@ +package cvm + +const ( + ExternalWithdrawVirtrualInstructionDataSize = SignatureSize // signature +) + +type ExternalWithdrawVirtualInstructionArgs struct { + Signature Signature +} + +func NewExternalWithdrawVirtualInstruction( + args *ExternalWithdrawVirtualInstructionArgs, +) VirtualInstruction { + var offset int + data := make([]byte, ExternalWithdrawVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + + return VirtualInstruction{ + Opcode: OpcodeExternalWithdraw, + Data: data, + } +} diff --git a/pkg/solana/cvm/virtual_instructions_relay.go b/pkg/solana/cvm/virtual_instructions_relay.go new file mode 100644 index 00000000..237ca037 --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_relay.go @@ -0,0 +1,32 @@ +package cvm + +const ( + RelayVirtrualInstructionDataSize = (8 + // amount + HashSize + // transcript + HashSize + // recent_root + HashSize) // commitment +) + +type RelayVirtualInstructionArgs struct { + Amount uint64 + Transcript Hash + RecentRoot Hash + Commitment Hash +} + +func NewRelayVirtualInstruction( + args *RelayVirtualInstructionArgs, +) VirtualInstruction { + var offset int + data := make([]byte, RelayVirtrualInstructionDataSize) + + putUint64(data, args.Amount, &offset) + putHash(data, args.Transcript, &offset) + putHash(data, args.RecentRoot, &offset) + putHash(data, args.Commitment, &offset) + + return VirtualInstruction{ + Opcode: OpcodeRelay, + Data: data, + } +} diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go deleted file mode 100644 index 6b272229..00000000 --- a/pkg/solana/cvm/virtual_instructions_relay_transfer_external.go +++ /dev/null @@ -1,39 +0,0 @@ -package cvm - -import ( - "github.com/code-payments/code-server/pkg/solana" -) - -const ( - RelayTransferExternalVirtrualInstructionDataSize = (8 + // amount - HashSize + // transcript - HashSize + // recent_root - HashSize) // commitment -) - -type RelayTransferExternalVirtualInstructionArgs struct { - Amount uint64 - Transcript Hash - RecentRoot Hash - Commitment Hash -} - -type RelayTransferExternalVirtualInstructionAccounts struct { -} - -func NewRelayTransferExternalVirtualInstructionCtor( - accounts *RelayTransferExternalVirtualInstructionAccounts, - args *RelayTransferExternalVirtualInstructionArgs, -) VirtualInstructionCtor { - return func() (Opcode, []solana.Instruction, []byte) { - var offset int - data := make([]byte, RelayTransferExternalVirtrualInstructionDataSize) - - putUint64(data, args.Amount, &offset) - putHash(data, args.Transcript, &offset) - putHash(data, args.RecentRoot, &offset) - putHash(data, args.Commitment, &offset) - - return OpcodeSplitterTransferToExternal, nil, data - } -} diff --git a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go deleted file mode 100644 index 6dc4bc7b..00000000 --- a/pkg/solana/cvm/virtual_instructions_relay_transfer_internal.go +++ /dev/null @@ -1,39 +0,0 @@ -package cvm - -import ( - "github.com/code-payments/code-server/pkg/solana" -) - -const ( - RelayTransferInternalVirtrualInstructionDataSize = (8 + // amount - HashSize + // transcript - HashSize + // recent_root - HashSize) // commitment -) - -type RelayTransferInternalVirtualInstructionArgs struct { - Amount uint64 - Transcript Hash - RecentRoot Hash - Commitment Hash -} - -type RelayTransferInternalVirtualInstructionAccounts struct { -} - -func NewRelayTransferInternalVirtualInstructionCtor( - accounts *RelayTransferInternalVirtualInstructionAccounts, - args *RelayTransferInternalVirtualInstructionArgs, -) VirtualInstructionCtor { - return func() (Opcode, []solana.Instruction, []byte) { - var offset int - data := make([]byte, RelayTransferInternalVirtrualInstructionDataSize) - - putUint64(data, args.Amount, &offset) - putHash(data, args.Transcript, &offset) - putHash(data, args.RecentRoot, &offset) - putHash(data, args.Commitment, &offset) - - return OpcodeSplitterTransferToInternal, nil, data - } -} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go deleted file mode 100644 index 1daddac2..00000000 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_external.go +++ /dev/null @@ -1,59 +0,0 @@ -package cvm - -import ( - "crypto/ed25519" - - "github.com/code-payments/code-server/pkg/solana" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" -) - -const ( - TimelockTransferExternalVirtrualInstructionDataSize = (SignatureSize + // signature - 8) // amount -) - -type TimelockTransferExternalVirtualInstructionArgs struct { - TimelockBump uint8 - Amount uint64 - Signature Signature -} - -type TimelockTransferExternalVirtualInstructionAccounts struct { - VmAuthority ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - VirtualTimelockVault ed25519.PublicKey - Owner ed25519.PublicKey - Destination ed25519.PublicKey -} - -func NewTimelockTransferExternalVirtualInstructionCtor( - accounts *TimelockTransferExternalVirtualInstructionAccounts, - args *TimelockTransferExternalVirtualInstructionArgs, -) VirtualInstructionCtor { - return func() (Opcode, []solana.Instruction, []byte) { - var offset int - data := make([]byte, TimelockTransferExternalVirtrualInstructionDataSize) - putSignature(data, args.Signature, &offset) - putUint64(data, args.Amount, &offset) - - ixns := []solana.Instruction{ - newKreMemoIxn(), - timelock_token.NewTransferWithAuthorityInstruction( - &timelock_token.TransferWithAuthorityInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - VaultOwner: accounts.Owner, - TimeAuthority: accounts.VmAuthority, - Destination: accounts.Destination, - Payer: accounts.VmAuthority, - }, - &timelock_token.TransferWithAuthorityInstructionArgs{ - TimelockBump: args.TimelockBump, - Amount: uint64(args.Amount), - }, - ).ToLegacyInstruction(), - } - - return OpcodeTimelockTransferToExternal, ixns, data - } -} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go deleted file mode 100644 index 4d2ca2b6..00000000 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_internal.go +++ /dev/null @@ -1,59 +0,0 @@ -package cvm - -import ( - "crypto/ed25519" - - "github.com/code-payments/code-server/pkg/solana" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" -) - -const ( - TimelockTransferInternalVirtrualInstructionDataSize = (SignatureSize + // signature - 8) // amount -) - -type TimelockTransferInternalVirtualInstructionArgs struct { - TimelockBump uint8 - Amount uint64 - Signature Signature -} - -type TimelockTransferInternalVirtualInstructionAccounts struct { - VmAuthority ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - VirtualTimelockVault ed25519.PublicKey - Owner ed25519.PublicKey - Destination ed25519.PublicKey -} - -func NewTimelockTransferInternalVirtualInstructionCtor( - accounts *TimelockTransferInternalVirtualInstructionAccounts, - args *TimelockTransferInternalVirtualInstructionArgs, -) VirtualInstructionCtor { - return func() (Opcode, []solana.Instruction, []byte) { - var offset int - data := make([]byte, TimelockTransferInternalVirtrualInstructionDataSize) - putSignature(data, args.Signature, &offset) - putUint64(data, args.Amount, &offset) - - ixns := []solana.Instruction{ - newKreMemoIxn(), - timelock_token.NewTransferWithAuthorityInstruction( - &timelock_token.TransferWithAuthorityInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - VaultOwner: accounts.Owner, - TimeAuthority: accounts.VmAuthority, - Destination: accounts.Destination, - Payer: accounts.VmAuthority, - }, - &timelock_token.TransferWithAuthorityInstructionArgs{ - TimelockBump: args.TimelockBump, - Amount: uint64(args.Amount), - }, - ).ToLegacyInstruction(), - } - - return OpcodeTimelockTransferToInternal, ixns, data - } -} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go b/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go deleted file mode 100644 index 935b58e9..00000000 --- a/pkg/solana/cvm/virtual_instructions_timelock_transfer_relay.go +++ /dev/null @@ -1,59 +0,0 @@ -package cvm - -import ( - "crypto/ed25519" - - "github.com/code-payments/code-server/pkg/solana" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" -) - -const ( - TimelockTransferRelayVirtrualInstructionDataSize = (SignatureSize + // signature - 8) // amount -) - -type TimelockTransferRelayVirtualInstructionArgs struct { - TimelockBump uint8 - Amount uint64 - Signature Signature -} - -type TimelockTransferRelayVirtualInstructionAccounts struct { - VmAuthority ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - VirtualTimelockVault ed25519.PublicKey - Owner ed25519.PublicKey - RelayVault ed25519.PublicKey -} - -func NewTimelockTransferRelayVirtualInstructionCtor( - accounts *TimelockTransferRelayVirtualInstructionAccounts, - args *TimelockTransferRelayVirtualInstructionArgs, -) VirtualInstructionCtor { - return func() (Opcode, []solana.Instruction, []byte) { - var offset int - data := make([]byte, TimelockTransferRelayVirtrualInstructionDataSize) - putSignature(data, args.Signature, &offset) - putUint64(data, args.Amount, &offset) - - ixns := []solana.Instruction{ - newKreMemoIxn(), - timelock_token.NewTransferWithAuthorityInstruction( - &timelock_token.TransferWithAuthorityInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - VaultOwner: accounts.Owner, - TimeAuthority: accounts.VmAuthority, - Destination: accounts.RelayVault, - Payer: accounts.VmAuthority, - }, - &timelock_token.TransferWithAuthorityInstructionArgs{ - TimelockBump: args.TimelockBump, - Amount: uint64(args.Amount), - }, - ).ToLegacyInstruction(), - } - - return OpcodeTimelockTransferToRelay, ixns, data - } -} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go b/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go deleted file mode 100644 index 8362ac9a..00000000 --- a/pkg/solana/cvm/virtual_instructions_timelock_withdraw_external.go +++ /dev/null @@ -1,87 +0,0 @@ -package cvm - -import ( - "crypto/ed25519" - - "github.com/code-payments/code-server/pkg/solana" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" -) - -const ( - TimelockWithdrawEnternalVirtrualInstructionDataSize = SignatureSize // signature -) - -type TimelockWithdrawExternalVirtualInstructionArgs struct { - TimelockBump uint8 - Signature Signature -} - -type TimelockWithdrawExternalVirtualInstructionAccounts struct { - VmAuthority ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - VirtualTimelockVault ed25519.PublicKey - Destination ed25519.PublicKey - Owner ed25519.PublicKey - Mint ed25519.PublicKey -} - -func NewTimelockWithdrawExternalVirtualInstructionCtor( - accounts *TimelockWithdrawExternalVirtualInstructionAccounts, - args *TimelockWithdrawExternalVirtualInstructionArgs, -) VirtualInstructionCtor { - return func() (Opcode, []solana.Instruction, []byte) { - var offset int - data := make([]byte, TimelockWithdrawEnternalVirtrualInstructionDataSize) - putSignature(data, args.Signature, &offset) - - ixns := []solana.Instruction{ - newKreMemoIxn(), - timelock_token.NewRevokeLockWithAuthorityInstruction( - &timelock_token.RevokeLockWithAuthorityInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - TimeAuthority: accounts.VmAuthority, - Payer: accounts.VmAuthority, - }, - &timelock_token.RevokeLockWithAuthorityInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - timelock_token.NewDeactivateInstruction( - &timelock_token.DeactivateInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - VaultOwner: accounts.Owner, - Payer: accounts.VmAuthority, - }, - &timelock_token.DeactivateInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - timelock_token.NewWithdrawInstruction( - &timelock_token.WithdrawInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - VaultOwner: accounts.Owner, - Destination: accounts.Destination, - Payer: accounts.VmAuthority, - }, - &timelock_token.WithdrawInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - timelock_token.NewCloseAccountsInstruction( - &timelock_token.CloseAccountsInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - CloseAuthority: accounts.VmAuthority, - Payer: accounts.VmAuthority, - }, - &timelock_token.CloseAccountsInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - } - - return OpcodeTimelockWithdrawToExternal, ixns, data - } -} diff --git a/pkg/solana/cvm/virtual_instructions_timelock_withdraw_internal.go b/pkg/solana/cvm/virtual_instructions_timelock_withdraw_internal.go deleted file mode 100644 index 90001c8c..00000000 --- a/pkg/solana/cvm/virtual_instructions_timelock_withdraw_internal.go +++ /dev/null @@ -1,87 +0,0 @@ -package cvm - -import ( - "crypto/ed25519" - - "github.com/code-payments/code-server/pkg/solana" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" -) - -const ( - TimelockWithdrawInternalVirtrualInstructionDataSize = SignatureSize // signature -) - -type TimelockWithdrawInternalVirtualInstructionArgs struct { - TimelockBump uint8 - Signature Signature -} - -type TimelockWithdrawInternalVirtualInstructionAccounts struct { - VmAuthority ed25519.PublicKey - VirtualTimelock ed25519.PublicKey - VirtualTimelockVault ed25519.PublicKey - Destination ed25519.PublicKey - Owner ed25519.PublicKey - Mint ed25519.PublicKey -} - -func NewTimelockWithdrawInternalVirtualInstructionCtor( - accounts *TimelockWithdrawInternalVirtualInstructionAccounts, - args *TimelockWithdrawInternalVirtualInstructionArgs, -) VirtualInstructionCtor { - return func() (Opcode, []solana.Instruction, []byte) { - var offset int - data := make([]byte, TimelockWithdrawInternalVirtrualInstructionDataSize) - putSignature(data, args.Signature, &offset) - - ixns := []solana.Instruction{ - newKreMemoIxn(), - timelock_token.NewRevokeLockWithAuthorityInstruction( - &timelock_token.RevokeLockWithAuthorityInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - TimeAuthority: accounts.VmAuthority, - Payer: accounts.VmAuthority, - }, - &timelock_token.RevokeLockWithAuthorityInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - timelock_token.NewDeactivateInstruction( - &timelock_token.DeactivateInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - VaultOwner: accounts.Owner, - Payer: accounts.VmAuthority, - }, - &timelock_token.DeactivateInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - timelock_token.NewWithdrawInstruction( - &timelock_token.WithdrawInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - VaultOwner: accounts.Owner, - Destination: accounts.Destination, - Payer: accounts.VmAuthority, - }, - &timelock_token.WithdrawInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - timelock_token.NewCloseAccountsInstruction( - &timelock_token.CloseAccountsInstructionAccounts{ - Timelock: accounts.VirtualTimelock, - Vault: accounts.VirtualTimelockVault, - CloseAuthority: accounts.VmAuthority, - Payer: accounts.VmAuthority, - }, - &timelock_token.CloseAccountsInstructionArgs{ - TimelockBump: args.TimelockBump, - }, - ).ToLegacyInstruction(), - } - - return OpcodeTimelockWithdrawToInternal, ixns, data - } -} diff --git a/pkg/solana/cvm/virtual_instructions_transfer.go b/pkg/solana/cvm/virtual_instructions_transfer.go new file mode 100644 index 00000000..8e3d6205 --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_transfer.go @@ -0,0 +1,25 @@ +package cvm + +const ( + TransferVirtrualInstructionDataSize = (SignatureSize + // signature + 8) // amount +) + +type TransferVirtualInstructionArgs struct { + Amount uint64 + Signature Signature +} + +func NewTransferVirtualInstruction( + args *TransferVirtualInstructionArgs, +) VirtualInstruction { + var offset int + data := make([]byte, TransferVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + putUint64(data, args.Amount, &offset) + + return VirtualInstruction{ + Opcode: OpcodeTransfer, + Data: data, + } +} diff --git a/pkg/solana/cvm/virtual_instructions_withdraw.go b/pkg/solana/cvm/virtual_instructions_withdraw.go new file mode 100644 index 00000000..dda33567 --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_withdraw.go @@ -0,0 +1,22 @@ +package cvm + +const ( + WithdrawVirtrualInstructionDataSize = SignatureSize // signature +) + +type WithdrawVirtualInstructionArgs struct { + Signature Signature +} + +func NewWithdrawVirtualInstruction( + args *WithdrawVirtualInstructionArgs, +) VirtualInstruction { + var offset int + data := make([]byte, WithdrawVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + + return VirtualInstruction{ + Opcode: OpcodeWithdraw, + Data: data, + } +} From b90f9f2197245a0e3f5d28144ca38db06d42842e Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 28 Oct 2024 15:54:15 -0400 Subject: [PATCH 57/79] Updates to VM nonces and messages --- pkg/code/async/sequencer/vm.go | 2 +- .../grpc/transaction/v2/action_handler.go | 34 +++++++++++-------- .../server/grpc/transaction/v2/airdrop.go | 9 ++--- pkg/solana/cvm/types_message.go | 23 +++++++------ .../cvm/virtual_accounts_durable_nonce.go | 10 +++--- 5 files changed, 43 insertions(+), 35 deletions(-) diff --git a/pkg/code/async/sequencer/vm.go b/pkg/code/async/sequencer/vm.go index 473ec02d..f4499cf5 100644 --- a/pkg/code/async/sequencer/vm.go +++ b/pkg/code/async/sequencer/vm.go @@ -133,7 +133,7 @@ func getVirtualDurableNonceAccountStateInMemory(ctx context.Context, vmIndexerCl protoAccount := resp.Item.Account state := cvm.VirtualDurableNonce{ Address: protoAccount.Address.Value, - Nonce: cvm.Hash(protoAccount.Nonce.Value), + Value: cvm.Hash(protoAccount.Nonce.Value), } return &state, memory, uint16(protoMemory.Index), nil diff --git a/pkg/code/server/grpc/transaction/v2/action_handler.go b/pkg/code/server/grpc/transaction/v2/action_handler.go index 5d813b4e..dbab2b99 100644 --- a/pkg/code/server/grpc/transaction/v2/action_handler.go +++ b/pkg/code/server/grpc/transaction/v2/action_handler.go @@ -329,10 +329,11 @@ func (h *NoPrivacyTransferActionHandler) GetFulfillmentMetadata( switch index { case 0: virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ - Source: h.source.Vault.PublicKey().ToBytes(), - Destination: h.destination.PublicKey().ToBytes(), - Amount: h.amount, - Nonce: cvm.Hash(bh), + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.destination.PublicKey().ToBytes(), + Amount: h.amount, + NonceAddress: nonce.PublicKey().ToBytes(), + NonceValue: cvm.Hash(bh), }) return &newFulfillmentMetadata{ @@ -431,9 +432,10 @@ func (h *NoPrivacyWithdrawActionHandler) GetFulfillmentMetadata( switch index { case 0: virtualIxnHash := cvm.GetCompactWithdrawMessage(&cvm.GetCompactWithdrawMessageArgs{ - Source: h.source.Vault.PublicKey().ToBytes(), - Destination: h.destination.PublicKey().ToBytes(), - Nonce: cvm.Hash(bh), + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.destination.PublicKey().ToBytes(), + NonceAddress: nonce.PublicKey().ToBytes(), + NonceValue: cvm.Hash(bh), }) return &newFulfillmentMetadata{ @@ -688,10 +690,11 @@ func (h *TemporaryPrivacyTransferActionHandler) GetFulfillmentMetadata( }, nil case 1: virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ - Source: h.source.Vault.PublicKey().ToBytes(), - Destination: h.commitmentVault.PublicKey().ToBytes(), - Amount: h.unsavedCommitmentRecord.Amount, - Nonce: cvm.Hash(bh), + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.commitmentVault.PublicKey().ToBytes(), + Amount: h.unsavedCommitmentRecord.Amount, + NonceAddress: nonce.PublicKey().ToBytes(), + NonceValue: cvm.Hash(bh), }) return &newFulfillmentMetadata{ @@ -827,10 +830,11 @@ func (h *PermanentPrivacyUpgradeActionHandler) GetFulfillmentMetadata( bh solana.Blockhash, ) (*newFulfillmentMetadata, error) { virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ - Source: h.source.Vault.PublicKey().ToBytes(), - Destination: h.privacyUpgradeProof.newCommitmentVault.PublicKey().ToBytes(), - Amount: h.commitmentBeingUpgraded.Amount, - Nonce: cvm.Hash(bh), + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.privacyUpgradeProof.newCommitmentVault.PublicKey().ToBytes(), + Amount: h.commitmentBeingUpgraded.Amount, + NonceAddress: nonce.PublicKey().ToBytes(), + NonceValue: cvm.Hash(bh), }) return &newFulfillmentMetadata{ diff --git a/pkg/code/server/grpc/transaction/v2/airdrop.go b/pkg/code/server/grpc/transaction/v2/airdrop.go index 6592dc5f..befbfdfc 100644 --- a/pkg/code/server/grpc/transaction/v2/airdrop.go +++ b/pkg/code/server/grpc/transaction/v2/airdrop.go @@ -273,10 +273,11 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner }() vixnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ - Source: s.airdropper.Vault.PublicKey().ToBytes(), - Destination: destination.PublicKey().ToBytes(), - Amount: quarkAmount, - Nonce: cvm.Hash(selectedNonce.Blockhash), + Source: s.airdropper.Vault.PublicKey().ToBytes(), + Destination: destination.PublicKey().ToBytes(), + Amount: quarkAmount, + NonceAddress: selectedNonce.Account.PublicKey().ToBytes(), + NonceValue: cvm.Hash(selectedNonce.Blockhash), }) virtualSig := ed25519.Sign(s.airdropper.VaultOwner.PrivateKey().ToBytes(), vixnHash[:]) diff --git a/pkg/solana/cvm/types_message.go b/pkg/solana/cvm/types_message.go index 07394b9f..80c2b191 100644 --- a/pkg/solana/cvm/types_message.go +++ b/pkg/solana/cvm/types_message.go @@ -11,10 +11,11 @@ type Message []byte type CompactMessage Hash type GetCompactTransferMessageArgs struct { - Source ed25519.PublicKey - Destination ed25519.PublicKey - Amount uint64 - Nonce Hash + Source ed25519.PublicKey + Destination ed25519.PublicKey + Amount uint64 + NonceAddress ed25519.PublicKey + NonceValue Hash } func GetCompactTransferMessage(args *GetCompactTransferMessageArgs) CompactMessage { @@ -26,14 +27,16 @@ func GetCompactTransferMessage(args *GetCompactTransferMessageArgs) CompactMessa message = append(message, args.Source...) message = append(message, args.Destination...) message = append(message, amountBytes...) - message = append(message, args.Nonce[:]...) + message = append(message, args.NonceAddress...) + message = append(message, args.NonceValue[:]...) return hashMessage(message) } type GetCompactWithdrawMessageArgs struct { - Source ed25519.PublicKey - Destination ed25519.PublicKey - Nonce Hash + Source ed25519.PublicKey + Destination ed25519.PublicKey + NonceAddress ed25519.PublicKey + NonceValue Hash } func GetCompactWithdrawMessage(args *GetCompactWithdrawMessageArgs) CompactMessage { @@ -41,8 +44,8 @@ func GetCompactWithdrawMessage(args *GetCompactWithdrawMessageArgs) CompactMessa message = append(message, []byte("withdraw_and_close")...) message = append(message, args.Source...) message = append(message, args.Destination...) - - message = append(message, args.Nonce[:]...) + message = append(message, args.NonceAddress...) + message = append(message, args.NonceValue[:]...) return hashMessage(message) } diff --git a/pkg/solana/cvm/virtual_accounts_durable_nonce.go b/pkg/solana/cvm/virtual_accounts_durable_nonce.go index 4e4d48ec..e10505d7 100644 --- a/pkg/solana/cvm/virtual_accounts_durable_nonce.go +++ b/pkg/solana/cvm/virtual_accounts_durable_nonce.go @@ -12,7 +12,7 @@ const VirtualDurableNonceSize = (32 + // address type VirtualDurableNonce struct { Address ed25519.PublicKey - Nonce Hash + Value Hash } func (obj *VirtualDurableNonce) Marshal() []byte { @@ -21,7 +21,7 @@ func (obj *VirtualDurableNonce) Marshal() []byte { var offset int putKey(data, obj.Address, &offset) - putHash(data, obj.Nonce, &offset) + putHash(data, obj.Value, &offset) return data } @@ -34,7 +34,7 @@ func (obj *VirtualDurableNonce) UnmarshalDirectly(data []byte) error { var offset int getKey(data, &obj.Address, &offset) - getHash(data, &obj.Nonce, &offset) + getHash(data, &obj.Value, &offset) return nil } @@ -53,8 +53,8 @@ func (obj *VirtualDurableNonce) UnmarshalFromMemory(data []byte) error { func (obj *VirtualDurableNonce) String() string { return fmt.Sprintf( - "VirtualDurableNonce{address=%s,nonce=%s}", + "VirtualDurableNonce{address=%s,value=%s}", base58.Encode(obj.Address), - obj.Nonce.String(), + obj.Value.String(), ) } From aea08d3354a72f9e4969d29bbba639d5fcd63f9a Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 29 Oct 2024 09:06:34 -0400 Subject: [PATCH 58/79] Reintroduce GetPageSizeFromMemoryLayout --- pkg/solana/cvm/types_memory_layout.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/solana/cvm/types_memory_layout.go b/pkg/solana/cvm/types_memory_layout.go index c7a633ec..4e070bcb 100644 --- a/pkg/solana/cvm/types_memory_layout.go +++ b/pkg/solana/cvm/types_memory_layout.go @@ -17,3 +17,15 @@ func getMemoryLayout(src []byte, dst *MemoryLayout, offset *int) { *dst = MemoryLayout(src[*offset]) *offset += 1 } + +func GetPageSizeFromMemoryLayout(layout MemoryLayout) uint32 { + switch layout { + case MemoryLayoutNonce: + return GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce) + case MemoryLayoutTimelock: + return GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock) + case MemoryLayoutRelay: + return GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay) + } + return 0 +} From 27d6adcb7ef466ae1c6d65c2cf9913cac90f8ffa Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 29 Oct 2024 09:38:43 -0400 Subject: [PATCH 59/79] Fix InitMemoryInstructionArgsSize --- pkg/solana/cvm/instructions_init_memory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/solana/cvm/instructions_init_memory.go b/pkg/solana/cvm/instructions_init_memory.go index 1257f6f7..527f1112 100644 --- a/pkg/solana/cvm/instructions_init_memory.go +++ b/pkg/solana/cvm/instructions_init_memory.go @@ -7,7 +7,7 @@ import ( ) const ( - InitMemoryInstructionArgsSize = (4 + MaxMemoryAccountNameLength + // name + InitMemoryInstructionArgsSize = (MaxMemoryAccountNameLength + // name 1 + // layout 1) // vm_memory_bump ) From 69085d2aed9a27b3259db7a8c05477af21b148aa Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 29 Oct 2024 11:21:32 -0400 Subject: [PATCH 60/79] Fix MemoryAccountWithData Unmarshal for nonce layout --- pkg/solana/cvm/accounts_memory_account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index 33029980..d5b80bd2 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -98,7 +98,7 @@ func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { getSimpleMemoryAllocator(data, &obj.Data, capacity, itemSize, &offset) case MemoryLayoutNonce: capacity := CompactStateItems - itemSize := int(GetVirtualAccountSizeInMemory(VirtualDurableNonceSize)) + itemSize := int(GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce)) if len(data) < MemoryAccountSize+GetSimpleMemoryAllocatorSize(capacity, itemSize) { return ErrInvalidAccountData } From 98bf7e8d03ea5884c8adf04d8e46257e39dc84db Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 7 Nov 2024 10:11:34 -0500 Subject: [PATCH 61/79] Update padding position in memory account --- pkg/solana/cvm/accounts_memory_account.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index d5b80bd2..d47c44c1 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -32,8 +32,8 @@ const MemoryAccountSize = (8 + // discriminator 32 + // vm MaxMemoryAccountNameLength + // name 1 + // bump - 1 + // layout - 6) // padding + 6 + // padding + 1) // layout var MemoryAccountDiscriminator = []byte{byte(AccountTypeMemory), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} @@ -53,8 +53,8 @@ func (obj *MemoryAccount) Unmarshal(data []byte) error { getKey(data, &obj.Vm, &offset) getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) getUint8(data, &obj.Bump, &offset) - getMemoryLayout(data, &obj.Layout, &offset) offset += 6 // padding + getMemoryLayout(data, &obj.Layout, &offset) return nil } From 1183038a96b33e92fd6851334e41152702fcb1ca Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 7 Nov 2024 12:23:22 -0500 Subject: [PATCH 62/79] Update default compact state items to 1k --- pkg/solana/cvm/types_memory_allocator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/solana/cvm/types_memory_allocator.go b/pkg/solana/cvm/types_memory_allocator.go index 8b8f54bf..b56ded14 100644 --- a/pkg/solana/cvm/types_memory_allocator.go +++ b/pkg/solana/cvm/types_memory_allocator.go @@ -1,7 +1,7 @@ package cvm const ( - CompactStateItems = 100 + CompactStateItems = 1000 ) type MemoryAllocator interface { From a7dd419734f28e5dcd93f0509e5fb0238dc5c40a Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 7 Nov 2024 12:59:31 -0500 Subject: [PATCH 63/79] Bump memory DB record page and sector count to uint16 --- pkg/code/data/cvm/ram/account.go | 4 ++-- pkg/code/data/cvm/ram/postgres/model.go | 4 ++-- pkg/code/data/cvm/ram/util_test.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/code/data/cvm/ram/account.go b/pkg/code/data/cvm/ram/account.go index cb7bd559..1760e48b 100644 --- a/pkg/code/data/cvm/ram/account.go +++ b/pkg/code/data/cvm/ram/account.go @@ -15,8 +15,8 @@ type Record struct { Address string Capacity uint16 - NumSectors uint8 - NumPages uint8 + NumSectors uint16 + NumPages uint16 PageSize uint8 StoredAccountType cvm.VirtualAccountType diff --git a/pkg/code/data/cvm/ram/postgres/model.go b/pkg/code/data/cvm/ram/postgres/model.go index 45188417..db0b3ee7 100644 --- a/pkg/code/data/cvm/ram/postgres/model.go +++ b/pkg/code/data/cvm/ram/postgres/model.go @@ -25,8 +25,8 @@ type accountModel struct { Address string `db:"address"` Capacity uint16 `db:"capacity"` - NumSectors uint8 `db:"num_sectors"` - NumPages uint8 `db:"num_pages"` + NumSectors uint16 `db:"num_sectors"` + NumPages uint16 `db:"num_pages"` PageSize uint8 `db:"page_size"` StoredAccountType uint8 `db:"stored_account_type"` diff --git a/pkg/code/data/cvm/ram/util_test.go b/pkg/code/data/cvm/ram/util_test.go index fa2f167c..a298ee40 100644 --- a/pkg/code/data/cvm/ram/util_test.go +++ b/pkg/code/data/cvm/ram/util_test.go @@ -11,8 +11,8 @@ import ( func TestGetActualCapcity(t *testing.T) { for _, tc := range []struct { capacity uint16 - numSectors uint8 - numPages uint8 + numSectors uint16 + numPages uint16 pageSize uint8 expected uint16 }{ From 7902dd08493ad367aa34daccd86638ca3f076861 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 7 Nov 2024 14:02:18 -0500 Subject: [PATCH 64/79] Fix MemoryAccountWithData unmarshalling --- pkg/solana/cvm/accounts_memory_account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index d47c44c1..7609d90e 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -85,8 +85,8 @@ func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { getKey(data, &obj.Vm, &offset) getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) getUint8(data, &obj.Bump, &offset) - getMemoryLayout(data, &obj.Layout, &offset) offset += 6 // padding + getMemoryLayout(data, &obj.Layout, &offset) switch obj.Layout { case MemoryLayoutTimelock: From 973241dc9273f6a8a43624b93232590a8521b685 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 25 Nov 2024 14:14:22 -0500 Subject: [PATCH 65/79] Setup real program address and configuration --- pkg/solana/cvm/program.go | 3 +-- pkg/solana/cvm/types_memory_allocator.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/solana/cvm/program.go b/pkg/solana/cvm/program.go index 94e7c15e..2cc2f717 100644 --- a/pkg/solana/cvm/program.go +++ b/pkg/solana/cvm/program.go @@ -14,8 +14,7 @@ var ( ) var ( - // todo: setup real program address - PROGRAM_ADDRESS = mustBase58Decode("vmT2hAx4N2U6DyjYxgQHER4VGC8tHJCfHNsSepBKCJZ") + PROGRAM_ADDRESS = mustBase58Decode("vmZ1WUq8SxjBWcaeTCvgJRZbS84R61uniFsQy5YMRTJ") PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) ) diff --git a/pkg/solana/cvm/types_memory_allocator.go b/pkg/solana/cvm/types_memory_allocator.go index b56ded14..0b6703fc 100644 --- a/pkg/solana/cvm/types_memory_allocator.go +++ b/pkg/solana/cvm/types_memory_allocator.go @@ -1,7 +1,7 @@ package cvm const ( - CompactStateItems = 1000 + CompactStateItems = 32_000 ) type MemoryAllocator interface { From cdc48b5468f6a1bd9df342a5c1de7798e25e787d Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 18 Dec 2024 15:12:19 -0500 Subject: [PATCH 66/79] New memory account structure (#190) * Update init memory instruction args * Hanlde unmarshalling both version of memory accounts * Reduce duplicated code for unmarshalling memory account structs * Rename simple allocator to slice allocator --- pkg/solana/cvm/accounts_memory_account.go | 117 ++++++++++-------- pkg/solana/cvm/instructions_init_memory.go | 9 +- pkg/solana/cvm/types_memory_allocator.go | 2 +- pkg/solana/cvm/types_memory_version.go | 17 +++ ..._allocator.go => types_slice_allocator.go} | 20 +-- pkg/solana/cvm/utils.go | 4 + 6 files changed, 101 insertions(+), 68 deletions(-) create mode 100644 pkg/solana/cvm/types_memory_version.go rename pkg/solana/cvm/{types_simple_memory_allocator.go => types_slice_allocator.go} (62%) diff --git a/pkg/solana/cvm/accounts_memory_account.go b/pkg/solana/cvm/accounts_memory_account.go index 7609d90e..f000e66c 100644 --- a/pkg/solana/cvm/accounts_memory_account.go +++ b/pkg/solana/cvm/accounts_memory_account.go @@ -14,26 +14,31 @@ const ( ) type MemoryAccount struct { - Vm ed25519.PublicKey - Name string - Bump uint8 - Layout MemoryLayout + Vm ed25519.PublicKey + Name string + Bump uint8 + Version MemoryVersion + AccountSize uint16 + NumAccounts uint32 } type MemoryAccountWithData struct { - Vm ed25519.PublicKey - Name string - Bump uint8 - Layout MemoryLayout - Data SimpleMemoryAllocator // todo: support other implementations + Vm ed25519.PublicKey + Name string + Bump uint8 + Version MemoryVersion + AccountSize uint16 + NumAccounts uint32 + Data SliceAllocator // todo: support other implementations } const MemoryAccountSize = (8 + // discriminator 32 + // vm MaxMemoryAccountNameLength + // name 1 + // bump - 6 + // padding - 1) // layout + 1 + // version + 2 + // account_size + 4) // num_accounts var MemoryAccountDiscriminator = []byte{byte(AccountTypeMemory), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} @@ -53,77 +58,81 @@ func (obj *MemoryAccount) Unmarshal(data []byte) error { getKey(data, &obj.Vm, &offset) getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) getUint8(data, &obj.Bump, &offset) - offset += 6 // padding - getMemoryLayout(data, &obj.Layout, &offset) + getMemoryVersion(data, &obj.Version, &offset) + + switch obj.Version { + case MemoryVersionV0: + var layout MemoryLayout + offset += 5 // padding + getMemoryLayout(data, &layout, &offset) + + switch layout { + case MemoryLayoutTimelock: + obj.AccountSize = uint16(GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock)) + case MemoryLayoutNonce: + obj.AccountSize = uint16(GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce)) + case MemoryLayoutRelay: + obj.AccountSize = uint16(GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay)) + default: + return errors.New("unsupported memory layout") + } + obj.NumAccounts = MemoryV0NumAccounts + + case MemoryVersionV1: + getUint16(data, &obj.AccountSize, &offset) + getUint32(data, &obj.NumAccounts, &offset) + + default: + return errors.New("invalid memory account version") + } return nil } func (obj *MemoryAccount) String() string { return fmt.Sprintf( - "MemoryAccount{vm=%s,bump=%d,name=%s,layout=%d}", + "MemoryAccount{vm=%s,bump=%d,name=%s,version=%d,num_accounts=%d,account_size=%d}", base58.Encode(obj.Vm), obj.Bump, obj.Name, - obj.Layout, + obj.Version, + obj.NumAccounts, + obj.AccountSize, ) } func (obj *MemoryAccountWithData) Unmarshal(data []byte) error { - if len(data) < MemoryAccountSize { - return ErrInvalidAccountData + var memoryAccount MemoryAccount + if err := memoryAccount.Unmarshal(data); err != nil { + return err } - var offset int + obj.Vm = memoryAccount.Vm + obj.Name = memoryAccount.Name + obj.Bump = memoryAccount.Bump + obj.Version = memoryAccount.Version + obj.AccountSize = memoryAccount.AccountSize + obj.NumAccounts = memoryAccount.NumAccounts - var discriminator []byte - getDiscriminator(data, &discriminator, &offset) - if !bytes.Equal(discriminator, MemoryAccountDiscriminator) { + if len(data) < MemoryAccountSize+GetSliceAllocatorSize(int(obj.NumAccounts), int(obj.AccountSize)) { return ErrInvalidAccountData } - getKey(data, &obj.Vm, &offset) - getFixedString(data, &obj.Name, MaxMemoryAccountNameLength, &offset) - getUint8(data, &obj.Bump, &offset) - offset += 6 // padding - getMemoryLayout(data, &obj.Layout, &offset) - - switch obj.Layout { - case MemoryLayoutTimelock: - capacity := CompactStateItems - itemSize := int(GetVirtualAccountSizeInMemory(VirtualAccountTypeTimelock)) - if len(data) < MemoryAccountSize+GetSimpleMemoryAllocatorSize(capacity, itemSize) { - return ErrInvalidAccountData - } - getSimpleMemoryAllocator(data, &obj.Data, capacity, itemSize, &offset) - case MemoryLayoutNonce: - capacity := CompactStateItems - itemSize := int(GetVirtualAccountSizeInMemory(VirtualAccountTypeDurableNonce)) - if len(data) < MemoryAccountSize+GetSimpleMemoryAllocatorSize(capacity, itemSize) { - return ErrInvalidAccountData - } - getSimpleMemoryAllocator(data, &obj.Data, capacity, itemSize, &offset) - case MemoryLayoutRelay: - capacity := CompactStateItems - itemSize := int(GetVirtualAccountSizeInMemory(VirtualAccountTypeRelay)) - if len(data) < MemoryAccountSize+GetSimpleMemoryAllocatorSize(capacity, itemSize) { - return ErrInvalidAccountData - } - getSimpleMemoryAllocator(data, &obj.Data, capacity, itemSize, &offset) - default: - return errors.New("unsupported memory layout") - } + offset := MemoryAccountSize + getSliceAllocator(data, &obj.Data, int(obj.NumAccounts), int(obj.AccountSize), &offset) return nil } func (obj *MemoryAccountWithData) String() string { return fmt.Sprintf( - "MemoryAccountWithData{vm=%s,name=%s,bump=%d,layout=%d,data=%s}", + "MemoryAccountWithData{vm=%s,name=%s,bump=%d,version=%d,num_accounts=%d,account_size=%d,data=%s}", base58.Encode(obj.Vm), obj.Name, obj.Bump, - obj.Layout, + obj.Version, + obj.NumAccounts, + obj.AccountSize, obj.Data.String(), ) } diff --git a/pkg/solana/cvm/instructions_init_memory.go b/pkg/solana/cvm/instructions_init_memory.go index 527f1112..ba76e8eb 100644 --- a/pkg/solana/cvm/instructions_init_memory.go +++ b/pkg/solana/cvm/instructions_init_memory.go @@ -8,13 +8,15 @@ import ( const ( InitMemoryInstructionArgsSize = (MaxMemoryAccountNameLength + // name - 1 + // layout + 4 + // num_accounts + 2 + // account_size 1) // vm_memory_bump ) type InitMemoryInstructionArgs struct { Name string - Layout MemoryLayout + NumAccounts uint32 + AccountSize uint16 VmMemoryBump uint8 } @@ -35,7 +37,8 @@ func NewInitMemoryInstruction( putCodeInstruction(data, CodeInstructionInitMemory, &offset) putFixedString(data, args.Name, MaxMemoryAccountNameLength, &offset) - putMemoryLayout(data, args.Layout, &offset) + putUint32(data, args.NumAccounts, &offset) + putUint16(data, args.AccountSize, &offset) putUint8(data, args.VmMemoryBump, &offset) return solana.Instruction{ diff --git a/pkg/solana/cvm/types_memory_allocator.go b/pkg/solana/cvm/types_memory_allocator.go index 0b6703fc..47f7f9b1 100644 --- a/pkg/solana/cvm/types_memory_allocator.go +++ b/pkg/solana/cvm/types_memory_allocator.go @@ -1,7 +1,7 @@ package cvm const ( - CompactStateItems = 32_000 + MemoryV0NumAccounts = 32_000 ) type MemoryAllocator interface { diff --git a/pkg/solana/cvm/types_memory_version.go b/pkg/solana/cvm/types_memory_version.go new file mode 100644 index 00000000..381e354d --- /dev/null +++ b/pkg/solana/cvm/types_memory_version.go @@ -0,0 +1,17 @@ +package cvm + +type MemoryVersion uint8 + +const ( + MemoryVersionV0 MemoryVersion = iota + MemoryVersionV1 +) + +func putMemoryVersion(dst []byte, v MemoryVersion, offset *int) { + dst[*offset] = uint8(v) + *offset += 1 +} +func getMemoryVersion(src []byte, dst *MemoryVersion, offset *int) { + *dst = MemoryVersion(src[*offset]) + *offset += 1 +} diff --git a/pkg/solana/cvm/types_simple_memory_allocator.go b/pkg/solana/cvm/types_slice_allocator.go similarity index 62% rename from pkg/solana/cvm/types_simple_memory_allocator.go rename to pkg/solana/cvm/types_slice_allocator.go index d4fcd271..142f110b 100644 --- a/pkg/solana/cvm/types_simple_memory_allocator.go +++ b/pkg/solana/cvm/types_slice_allocator.go @@ -7,13 +7,13 @@ import ( "github.com/mr-tron/base58" ) -type SimpleMemoryAllocator struct { +type SliceAllocator struct { State ItemStateArray Data [][]byte } -func (obj *SimpleMemoryAllocator) Unmarshal(data []byte, capacity, itemSize int) error { - if len(data) < GetSimpleMemoryAllocatorSize(capacity, itemSize) { +func (obj *SliceAllocator) Unmarshal(data []byte, capacity, itemSize int) error { + if len(data) < GetSliceAllocatorSize(capacity, itemSize) { return ErrInvalidAccountData } @@ -30,19 +30,19 @@ func (obj *SimpleMemoryAllocator) Unmarshal(data []byte, capacity, itemSize int) return nil } -func getSimpleMemoryAllocator(src []byte, dst *SimpleMemoryAllocator, capacity, itemSize int, offset *int) { +func getSliceAllocator(src []byte, dst *SliceAllocator, capacity, itemSize int, offset *int) { dst.Unmarshal(src[*offset:], capacity, itemSize) - *offset += GetSimpleMemoryAllocatorSize(capacity, itemSize) + *offset += GetSliceAllocatorSize(capacity, itemSize) } -func (obj *SimpleMemoryAllocator) IsAllocated(index int) bool { +func (obj *SliceAllocator) IsAllocated(index int) bool { if index >= len(obj.State) { return false } return obj.State[index] == ItemStateAllocated } -func (obj *SimpleMemoryAllocator) Read(index int) ([]byte, bool) { +func (obj *SliceAllocator) Read(index int) ([]byte, bool) { if !obj.IsAllocated(index) { return nil, false } @@ -52,7 +52,7 @@ func (obj *SimpleMemoryAllocator) Read(index int) ([]byte, bool) { return copied, true } -func (obj *SimpleMemoryAllocator) String() string { +func (obj *SliceAllocator) String() string { dataStringValues := make([]string, len(obj.Data)) for i := 0; i < len(obj.Data); i++ { dataStringValues[i] = base58.Encode(obj.Data[i]) @@ -60,13 +60,13 @@ func (obj *SimpleMemoryAllocator) String() string { dataString := fmt.Sprintf("[%s]", strings.Join(dataStringValues, ",")) return fmt.Sprintf( - "SimpleMemoryAllocator{state=%s,data=%s}", + "SliceAllocator{state=%s,data=%s}", obj.State.String(), dataString, ) } -func GetSimpleMemoryAllocatorSize(capacity, itemSize int) int { +func GetSliceAllocatorSize(capacity, itemSize int) int { return (capacity*ItemStateSize + // state capacity*itemSize) // data } diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go index 7ace1e53..8d58fd06 100644 --- a/pkg/solana/cvm/utils.go +++ b/pkg/solana/cvm/utils.go @@ -107,6 +107,10 @@ func putUint32(dst []byte, v uint32, offset *int) { binary.LittleEndian.PutUint32(dst[*offset:], v) *offset += 4 } +func getUint32(src []byte, dst *uint32, offset *int) { + *dst = binary.LittleEndian.Uint32(src[*offset:]) + *offset += 4 +} func putUint64(dst []byte, v uint64, offset *int) { binary.LittleEndian.PutUint64(dst[*offset:], v) From 115948e033f98d8e87196fcc682b21700556a435 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 29 Jan 2025 11:21:50 -0500 Subject: [PATCH 67/79] Geyser backup worker for detecting unlocked Timelock accounts --- pkg/code/async/geyser/backup.go | 54 +++++++++++++-- pkg/code/async/geyser/config.go | 11 +--- pkg/code/async/geyser/metrics.go | 42 +++++++----- pkg/code/async/geyser/service.go | 76 ++++++++++++---------- pkg/code/async/geyser/timelock.go | 61 +++++++++++++++-- pkg/code/data/timelock/timelock.go | 2 +- pkg/solana/cvm/accounts_unlock_state.go | 70 ++++++++++++++++++++ pkg/solana/cvm/instructions_init_unlock.go | 74 +++++++++++++++++++++ pkg/solana/cvm/types_timelock_state.go | 24 +++++++ pkg/solana/cvm/utils.go | 5 ++ 10 files changed, 346 insertions(+), 73 deletions(-) create mode 100644 pkg/solana/cvm/accounts_unlock_state.go create mode 100644 pkg/solana/cvm/instructions_init_unlock.go create mode 100644 pkg/solana/cvm/types_timelock_state.go diff --git a/pkg/code/async/geyser/backup.go b/pkg/code/async/geyser/backup.go index 2bdc9f4e..6fbe82d3 100644 --- a/pkg/code/async/geyser/backup.go +++ b/pkg/code/async/geyser/backup.go @@ -2,12 +2,16 @@ package async_geyser import ( "context" + "sync" "time" "github.com/newrelic/go-agent/v3/newrelic" "github.com/code-payments/code-server/pkg/code/common" + "github.com/code-payments/code-server/pkg/code/data/timelock" + "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/metrics" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) // Backup system workers can be found here. This is necessary because we can't rely @@ -32,6 +36,8 @@ func (p *service) backupTimelockStateWorker(serviceCtx context.Context, interval }() delay := 0 * time.Second // Initially no delay, so we can run right after a deploy + cursor := query.EmptyCursor + oldestRecord := time.Now() for { select { case <-time.After(delay): @@ -41,15 +47,51 @@ func (p *service) backupTimelockStateWorker(serviceCtx context.Context, interval nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) m := nr.StartTransaction("async__geyser_consumer_service__backup_timelock_state_worker") defer m.End() - //tracedCtx := newrelic.NewContext(serviceCtx, m) + tracedCtx := newrelic.NewContext(serviceCtx, m) - jobSucceeded := false + timelockRecords, err := p.data.GetAllTimelocksByState( + tracedCtx, + timelock_token.StateLocked, + query.WithDirection(query.Ascending), + query.WithCursor(cursor), + query.WithLimit(100), + ) + if err == timelock.ErrTimelockNotFound { + p.metricStatusLock.Lock() + p.oldestTimelockRecord = &oldestRecord + p.metricStatusLock.Unlock() + + cursor = query.EmptyCursor + oldestRecord = time.Now() + return + } else if err != nil { + log.WithError(err).Warn("failed to get timelock records") + return + } - // todo: implement me + var wg sync.WaitGroup + for _, timelockRecord := range timelockRecords { + wg.Add(1) + + if timelockRecord.LastUpdatedAt.Before(oldestRecord) { + oldestRecord = timelockRecord.LastUpdatedAt + } + + go func(timelockRecord *timelock.Record) { + defer wg.Done() + + log := log.WithField("timelock", timelockRecord.Address) + + err := updateTimelockAccountRecord(tracedCtx, p.data, timelockRecord) + if err != nil { + log.WithError(err).Warn("failed to update timelock account") + } + }(timelockRecord) + } + + wg.Wait() - p.metricStatusLock.Lock() - p.unlockedTimelockAccountsSynced = jobSucceeded - p.metricStatusLock.Unlock() + cursor = query.ToCursor(timelockRecords[len(timelockRecords)-1].Id) }() delay = interval - time.Since(start) diff --git a/pkg/code/async/geyser/config.go b/pkg/code/async/geyser/config.go index 70628e88..2ca069df 100644 --- a/pkg/code/async/geyser/config.go +++ b/pkg/code/async/geyser/config.go @@ -19,11 +19,8 @@ const ( ProgramUpdateQueueSizeConfigEnvName = envConfigPrefix + "PROGRAM_UPDATE_QUEUE_SIZE" defaultProgramUpdateQueueSize = 1_000_000 - BackupTimelockWorkerDaysCheckedConfigEnvName = envConfigPrefix + "BACKUP_TIMELOCK_WORKER_DAYS_CHECKED" - defaultBackupTimelockWorkerDaysChecked = 5 - BackupTimelockWorkerIntervalConfigEnvName = envConfigPrefix + "BACKUP_TIMELOCK_WORKER_INTERVAL" - defaultBackupTimelockWorkerInterval = 8 * time.Hour + defaultBackupTimelockWorkerInterval = 1 * time.Minute BackupExternalDepositWorkerCountConfigEnvName = envConfigPrefix + "BACKUP_EXTERNAL_DEPOSIT_WORKER_COUNT" defaultBackupExternalDepositWorkerCount = 32 @@ -50,8 +47,7 @@ type conf struct { backupExternalDepositWorkerCount config.Uint64 backupExternalDepositWorkerInterval config.Duration - backupTimelockWorkerDaysChecked config.Uint64 - backupTimelockWorkerInterval config.Duration + backupTimelockWorkerInterval config.Duration messagingFeeCollectorPublicKey config.String backupMessagingWorkerInterval config.Duration @@ -74,8 +70,7 @@ func WithEnvConfigs() ConfigProvider { backupExternalDepositWorkerCount: env.NewUint64Config(BackupExternalDepositWorkerCountConfigEnvName, defaultBackupExternalDepositWorkerCount), backupExternalDepositWorkerInterval: env.NewDurationConfig(BackupExternalDepositWorkerIntervalConfigEnvName, defaultBackupExternalDepositWorkerInterval), - backupTimelockWorkerDaysChecked: env.NewUint64Config(BackupTimelockWorkerDaysCheckedConfigEnvName, defaultBackupTimelockWorkerDaysChecked), - backupTimelockWorkerInterval: env.NewDurationConfig(BackupTimelockWorkerIntervalConfigEnvName, defaultBackupTimelockWorkerInterval), + backupTimelockWorkerInterval: env.NewDurationConfig(BackupTimelockWorkerIntervalConfigEnvName, defaultBackupTimelockWorkerInterval), messagingFeeCollectorPublicKey: env.NewStringConfig(MessagingFeeCollectorPublicKeyConfigEnvName, defaultMessagingFeeCollectorPublicKey), backupMessagingWorkerInterval: env.NewDurationConfig(BackupMessagingWorkerIntervalConfigEnvName, defaultBackupMessagingWorkerInterval), diff --git a/pkg/code/async/geyser/metrics.go b/pkg/code/async/geyser/metrics.go index 21739727..2a3fe34a 100644 --- a/pkg/code/async/geyser/metrics.go +++ b/pkg/code/async/geyser/metrics.go @@ -35,11 +35,11 @@ func (p *service) metricsGaugeWorker(ctx context.Context) error { case <-time.After(delay): start := time.Now() - p.recordSubscriptionStatusPollingEvent(ctx) - p.recordEventWorkerStatusPollingEvent(ctx) - p.recordEventQueueStatusPollingEvent(ctx) + //p.recordSubscriptionStatusPollingEvent(ctx) + //p.recordEventWorkerStatusPollingEvent(ctx) + //p.recordEventQueueStatusPollingEvent(ctx) p.recordBackupWorkerStatusPollingEvent(ctx) - p.recordBackupQueueStatusPollingEvent(ctx) + //p.recordBackupQueueStatusPollingEvent(ctx) delay = time.Second - time.Since(start) } @@ -109,22 +109,30 @@ func (p *service) recordBackupWorkerStatusPollingEvent(ctx context.Context) { p.metricStatusLock.Lock() defer p.metricStatusLock.Unlock() + timelockMetrics := map[string]interface{}{ + "worker_type": timelockStateWorkerName, + "is_active": p.backupTimelockStateWorkerStatus, + } + if p.oldestTimelockRecord != nil { + timelockMetrics["oldest_record_age"] = int(time.Since(*p.oldestTimelockRecord) / time.Second) + p.oldestTimelockRecord = nil + } metrics.RecordEvent(ctx, backupWorkerStatusEventName, map[string]interface{}{ - "worker_type": timelockStateWorkerName, - "is_active": p.backupTimelockStateWorkerStatus, - "unlocked_timelock_accounts_synced": p.unlockedTimelockAccountsSynced, - }) - p.unlockedTimelockAccountsSynced = false - - metrics.RecordEvent(ctx, backupWorkerStatusEventName, map[string]interface{}{ - "worker_type": externalDepositWorkerName, - "is_active": p.backupExternalDepositWorkerStatus, + "worker_type": timelockStateWorkerName, + "is_active": p.backupTimelockStateWorkerStatus, }) - metrics.RecordEvent(ctx, backupWorkerStatusEventName, map[string]interface{}{ - "worker_type": messagingWorkerName, - "is_active": p.backupMessagingWorkerStatus, - }) + /* + metrics.RecordEvent(ctx, backupWorkerStatusEventName, map[string]interface{}{ + "worker_type": externalDepositWorkerName, + "is_active": p.backupExternalDepositWorkerStatus, + }) + + metrics.RecordEvent(ctx, backupWorkerStatusEventName, map[string]interface{}{ + "worker_type": messagingWorkerName, + "is_active": p.backupMessagingWorkerStatus, + }) + */ } func (p *service) recordBackupQueueStatusPollingEvent(ctx context.Context) { diff --git a/pkg/code/async/geyser/service.go b/pkg/code/async/geyser/service.go index b22450ba..bccb2a9e 100644 --- a/pkg/code/async/geyser/service.go +++ b/pkg/code/async/geyser/service.go @@ -35,8 +35,8 @@ type service struct { slotUpdateSubscriptionStatus bool highestObservedRootedSlot uint64 + oldestTimelockRecord *time.Time backupTimelockStateWorkerStatus bool - unlockedTimelockAccountsSynced bool backupExternalDepositWorkerStatus bool @@ -64,43 +64,47 @@ func (p *service) Start(ctx context.Context, _ time.Duration) error { p.log.WithError(err).Warn("timelock backup worker terminated unexpectedly") } }() - go func() { - err := p.backupExternalDepositWorker(ctx, p.conf.backupExternalDepositWorkerInterval.Get(ctx)) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("external deposit backup worker terminated unexpectedly") + /* + go func() { + err := p.backupExternalDepositWorker(ctx, p.conf.backupExternalDepositWorkerInterval.Get(ctx)) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("external deposit backup worker terminated unexpectedly") + } + }() + go func() { + err := p.backupMessagingWorker(ctx, p.conf.backupMessagingWorkerInterval.Get(ctx)) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("messaging backup worker terminated unexpectedly") + } + }() + + // Setup event worker goroutines + var wg sync.WaitGroup + for i := 0; i < int(p.conf.programUpdateWorkerCount.Get(ctx)); i++ { + wg.Add(1) + go func(id int) { + p.programUpdateWorker(ctx, id) + wg.Done() + }(i) } - }() - go func() { - err := p.backupMessagingWorker(ctx, p.conf.backupMessagingWorkerInterval.Get(ctx)) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("messaging backup worker terminated unexpectedly") - } - }() - - // Setup event worker goroutines - var wg sync.WaitGroup - for i := 0; i < int(p.conf.programUpdateWorkerCount.Get(ctx)); i++ { - wg.Add(1) - go func(id int) { - p.programUpdateWorker(ctx, id) - wg.Done() - }(i) - } + */ // Main event loops to consume updates from subscriptions to Geyser that // will be processed async - go func() { - err := p.consumeGeyserProgramUpdateEvents(ctx) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("geyser event consumer terminated unexpectedly") - } - }() - go func() { - err := p.consumeGeyserSlotUpdateEvents(ctx) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("geyser event consumer terminated unexpectedly") - } - }() + /* + go func() { + err := p.consumeGeyserProgramUpdateEvents(ctx) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("geyser event consumer terminated unexpectedly") + } + }() + go func() { + err := p.consumeGeyserSlotUpdateEvents(ctx) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("geyser event consumer terminated unexpectedly") + } + }() + */ // Start metrics gauge worker go func() { @@ -116,8 +120,8 @@ func (p *service) Start(ctx context.Context, _ time.Duration) error { } // Gracefully shutdown - close(p.programUpdatesChan) - wg.Wait() + //close(p.programUpdatesChan) + //wg.Wait() return nil } diff --git a/pkg/code/async/geyser/timelock.go b/pkg/code/async/geyser/timelock.go index 45c529d9..69dad069 100644 --- a/pkg/code/async/geyser/timelock.go +++ b/pkg/code/async/geyser/timelock.go @@ -2,14 +2,65 @@ package async_geyser import ( "context" + "time" - "github.com/pkg/errors" - + "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/code/data/timelock" + "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) -// todo: needs to be reimagined for the VM +func updateTimelockAccountRecord(ctx context.Context, data code_data.Provider, timelockRecord *timelock.Record) error { + // Wait for Timelock account initialization before monitoring state + // to avoid conflicting with the sequencer + if timelockRecord.VaultState == timelock_token.StateUnknown || timelockRecord.Block == 0 { + return nil + } + + unlockState, slot, err := getTimelockUnlockState(ctx, data, timelockRecord) + if err != nil { + return err + } + + if unlockState != nil { + timelockRecord.VaultState = timelock_token.StateWaitingForTimeout + if unlockState.IsUnlocked() { + timelockRecord.VaultState = timelock_token.StateUnlocked + } + + unlockAt := uint64(unlockState.UnlockAt) + timelockRecord.UnlockAt = &unlockAt + } + timelockRecord.Block = slot + timelockRecord.LastUpdatedAt = time.Now() + + return data.SaveTimelock(ctx, timelockRecord) +} + +func getTimelockUnlockState(ctx context.Context, data code_data.Provider, timelockRecord *timelock.Record) (*cvm.UnlockStateAccount, uint64, error) { + ownerAccount, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) + if err != nil { + return nil, 0, err + } + + timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + return nil, 0, err + } -func findUnlockedTimelockV1Accounts(ctx context.Context, data code_data.Provider, daysFromToday uint8) ([]string, uint64, error) { - return nil, 0, errors.New("not implemented") + marshalled, slot, err := data.GetBlockchainAccountDataAfterBlock(ctx, timelockAccounts.Unlock.PublicKey().ToBase58(), timelockRecord.Block) + switch err { + case nil: + var unlockState cvm.UnlockStateAccount + if err = unlockState.Unmarshal(marshalled); err != nil { + return nil, 0, err + } + return &unlockState, slot, nil + case solana.ErrNoAccountInfo: + return nil, slot, nil + default: + return nil, 0, err + } } diff --git a/pkg/code/data/timelock/timelock.go b/pkg/code/data/timelock/timelock.go index 701d07d8..6e4bb061 100644 --- a/pkg/code/data/timelock/timelock.go +++ b/pkg/code/data/timelock/timelock.go @@ -26,7 +26,7 @@ type Record struct { VaultAddress string VaultBump uint8 VaultOwner string - VaultState timelock_token_v1.TimelockState + VaultState timelock_token_v1.TimelockState // Uses the original Timelock account state since the CVM only defines enum states for unlock UnlockAt *uint64 diff --git a/pkg/solana/cvm/accounts_unlock_state.go b/pkg/solana/cvm/accounts_unlock_state.go new file mode 100644 index 00000000..ea547b9b --- /dev/null +++ b/pkg/solana/cvm/accounts_unlock_state.go @@ -0,0 +1,70 @@ +package cvm + +import ( + "bytes" + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const ( + UnlockStateAccountSize = (8 + //discriminator + 32 + // vm + 32 + // owner + 32 + // address + 1 + // bump + 1 + // state + 6) // padding +) + +var UnlockStateAccountDiscriminator = []byte{byte(AccountTypeUnlockState), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + +type UnlockStateAccount struct { + Vm ed25519.PublicKey + Owner ed25519.PublicKey + Address ed25519.PublicKey + UnlockAt int64 + Bump uint8 + State TimelockState +} + +func (obj *UnlockStateAccount) Unmarshal(data []byte) error { + if len(data) < UnlockStateAccountSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, UnlockStateAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.Vm, &offset) + getKey(data, &obj.Owner, &offset) + getKey(data, &obj.Address, &offset) + getInt64(data, &obj.UnlockAt, &offset) + getUint8(data, &obj.Bump, &offset) + getTimelockState(data, &obj.State, &offset) + offset += 6 // padding + + return nil +} + +func (obj *UnlockStateAccount) IsUnlocked() bool { + return obj.State == TimelockStateUnlocked +} + +func (obj *UnlockStateAccount) String() string { + return fmt.Sprintf( + "UnlockStateAccount{vm=%s,owner=%s,address=%s,unlock_at=%d,bump=%d,state=%s", + base58.Encode(obj.Vm), + base58.Encode(obj.Owner), + base58.Encode(obj.Address), + obj.UnlockAt, + obj.Bump, + obj.State.String(), + ) +} diff --git a/pkg/solana/cvm/instructions_init_unlock.go b/pkg/solana/cvm/instructions_init_unlock.go new file mode 100644 index 00000000..27bf3a79 --- /dev/null +++ b/pkg/solana/cvm/instructions_init_unlock.go @@ -0,0 +1,74 @@ +package cvm + +import ( + "crypto/ed25519" + + "github.com/code-payments/code-server/pkg/solana" +) + +const ( + InitUnlockInstructionArgsSize = 0 +) + +type InitUnlockInstructionArgs struct { +} + +type InitUnlockInstructionAccounts struct { + AccountOwner ed25519.PublicKey + Payer ed25519.PublicKey + Vm ed25519.PublicKey + UnlockState ed25519.PublicKey +} + +func NewInitUnlockInstruction( + accounts *InitUnlockInstructionAccounts, + args *InitUnlockInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, 1+InitUnlockInstructionArgsSize) + + putCodeInstruction(data, CodeInstructionInitUnlock, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.AccountOwner, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Payer, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Vm, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.UnlockState, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/pkg/solana/cvm/types_timelock_state.go b/pkg/solana/cvm/types_timelock_state.go new file mode 100644 index 00000000..cef8efe0 --- /dev/null +++ b/pkg/solana/cvm/types_timelock_state.go @@ -0,0 +1,24 @@ +package cvm + +type TimelockState uint8 + +const ( + TimelockStateUnknown TimelockState = iota + TimelockStateUnlocked + TimelockStateWaitingForTimeout +) + +func getTimelockState(src []byte, dst *TimelockState, offset *int) { + *dst = TimelockState(src[*offset]) + *offset += 1 +} + +func (s TimelockState) String() string { + switch s { + case TimelockStateUnlocked: + return "unlocked" + case TimelockStateWaitingForTimeout: + return "waiting_for_timeout" + } + return "unknown" +} diff --git a/pkg/solana/cvm/utils.go b/pkg/solana/cvm/utils.go index 8d58fd06..6681514e 100644 --- a/pkg/solana/cvm/utils.go +++ b/pkg/solana/cvm/utils.go @@ -121,6 +121,11 @@ func getUint64(src []byte, dst *uint64, offset *int) { *offset += 8 } +func getInt64(src []byte, dst *int64, offset *int) { + *dst = int64(binary.LittleEndian.Uint64(src[*offset:])) + *offset += 8 +} + func toFixedString(value string, length int) string { fixed := make([]byte, length) copy(fixed, []byte(value)) From 9c097b003b53ab1a62be3e9d5f46855f3b2af876 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 29 Jan 2025 12:33:31 -0500 Subject: [PATCH 68/79] Timelock backup tweaks and metric fix --- pkg/code/async/geyser/backup.go | 2 +- pkg/code/async/geyser/metrics.go | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/code/async/geyser/backup.go b/pkg/code/async/geyser/backup.go index 6fbe82d3..364337da 100644 --- a/pkg/code/async/geyser/backup.go +++ b/pkg/code/async/geyser/backup.go @@ -54,7 +54,7 @@ func (p *service) backupTimelockStateWorker(serviceCtx context.Context, interval timelock_token.StateLocked, query.WithDirection(query.Ascending), query.WithCursor(cursor), - query.WithLimit(100), + query.WithLimit(256), ) if err == timelock.ErrTimelockNotFound { p.metricStatusLock.Lock() diff --git a/pkg/code/async/geyser/metrics.go b/pkg/code/async/geyser/metrics.go index 2a3fe34a..739b182d 100644 --- a/pkg/code/async/geyser/metrics.go +++ b/pkg/code/async/geyser/metrics.go @@ -117,10 +117,7 @@ func (p *service) recordBackupWorkerStatusPollingEvent(ctx context.Context) { timelockMetrics["oldest_record_age"] = int(time.Since(*p.oldestTimelockRecord) / time.Second) p.oldestTimelockRecord = nil } - metrics.RecordEvent(ctx, backupWorkerStatusEventName, map[string]interface{}{ - "worker_type": timelockStateWorkerName, - "is_active": p.backupTimelockStateWorkerStatus, - }) + metrics.RecordEvent(ctx, backupWorkerStatusEventName, timelockMetrics) /* metrics.RecordEvent(ctx, backupWorkerStatusEventName, map[string]interface{}{ From 02eed8a5eaee1f0045bbd42f3822fc42b18a6e9e Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 29 Jan 2025 13:31:20 -0500 Subject: [PATCH 69/79] Ensure Timelock accounts aren't unlocked when being opened --- .../grpc/transaction/v2/intent_handler.go | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/pkg/code/server/grpc/transaction/v2/intent_handler.go b/pkg/code/server/grpc/transaction/v2/intent_handler.go index f2bb112d..cc2e92e7 100644 --- a/pkg/code/server/grpc/transaction/v2/intent_handler.go +++ b/pkg/code/server/grpc/transaction/v2/intent_handler.go @@ -33,6 +33,7 @@ import ( "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" push_lib "github.com/code-payments/code-server/pkg/push" + "github.com/code-payments/code-server/pkg/solana" ) var accountTypesToOpen = []commonpb.AccountType{ @@ -218,7 +219,7 @@ func (h *OpenAccountsIntentHandler) AllowCreation(ctx context.Context, intentRec // Part 4: Validate the individual actions // - err = h.validateActions(initiatiorOwnerAccount, actions) + err = h.validateActions(ctx, initiatiorOwnerAccount, actions) if err != nil { return err } @@ -239,7 +240,7 @@ func (h *OpenAccountsIntentHandler) AllowCreation(ctx context.Context, intentRec return validateFeePayments(ctx, h.data, intentRecord, simResult) } -func (h *OpenAccountsIntentHandler) validateActions(initiatiorOwnerAccount *common.Account, actions []*transactionpb.Action) error { +func (h *OpenAccountsIntentHandler) validateActions(ctx context.Context, initiatiorOwnerAccount *common.Account, actions []*transactionpb.Action) error { expectedActionCount := len(accountTypesToOpen) if len(actions) != expectedActionCount { return newIntentValidationErrorf("expected %d total actions", expectedActionCount) @@ -283,6 +284,10 @@ func (h *OpenAccountsIntentHandler) validateActions(initiatiorOwnerAccount *comm if !bytes.Equal(openAction.GetOpenAccount().Token.Value, expectedVaultAccount.PublicKey().ToBytes()) { return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) } + + if err := validateTimelockUnlockStateDoesntExist(ctx, h.data, openAction.GetOpenAccount()); err != nil { + return err + } } return nil @@ -758,6 +763,8 @@ func (h *SendPrivatePaymentIntentHandler) validateActions( } err = validateGiftCardAccountOpened( + ctx, + h.data, initiatorOwnerAccount, initiatorAccountsByType, destination, @@ -2137,10 +2144,10 @@ func (h *EstablishRelationshipIntentHandler) AllowCreation(ctx context.Context, // Part 8: Validate the individual actions // - return h.validateActions(initiatiorOwnerAccount, actions) + return h.validateActions(ctx, initiatiorOwnerAccount, actions) } -func (h *EstablishRelationshipIntentHandler) validateActions(initiatiorOwnerAccount *common.Account, actions []*transactionpb.Action) error { +func (h *EstablishRelationshipIntentHandler) validateActions(ctx context.Context, initiatiorOwnerAccount *common.Account, actions []*transactionpb.Action) error { if len(actions) != 1 { return newIntentValidationError("expected 1 action") } @@ -2166,6 +2173,10 @@ func (h *EstablishRelationshipIntentHandler) validateActions(initiatiorOwnerAcco return newActionValidationErrorf(openAction, "authority cannot be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) } + if err := validateTimelockUnlockStateDoesntExist(ctx, h.data, openAction.GetOpenAccount()); err != nil { + return err + } + return nil } @@ -2476,6 +2487,8 @@ func validateNextTemporaryAccountOpened( // Assumes only one gift card account is opened per intent func validateGiftCardAccountOpened( + ctx context.Context, + data code_data.Provider, initiatorOwnerAccount *common.Account, initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, expectedGiftCardVault *common.Account, @@ -2524,6 +2537,10 @@ func validateGiftCardAccountOpened( return newActionValidationErrorf(openAction, "token must be %s", derivedVaultAccount.PublicKey().ToBase58()) } + if err := validateTimelockUnlockStateDoesntExist(ctx, data, openAction.GetOpenAccount()); err != nil { + return err + } + return nil } @@ -2861,6 +2878,28 @@ func validateTipDestination(ctx context.Context, data code_data.Provider, tipped return nil } +func validateTimelockUnlockStateDoesntExist(ctx context.Context, data code_data.Provider, openAction *transactionpb.OpenAccountAction) error { + authorityAccount, err := common.NewAccountFromProto(openAction.Authority) + if err != nil { + return err + } + + timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + return err + } + + _, err = data.GetBlockchainAccountInfo(ctx, timelockAccounts.Unlock.PublicKey().ToBase58(), solana.CommitmentFinalized) + switch err { + case nil: + return newIntentDeniedError("an account being opened has already initiated an unlock") + case solana.ErrNoAccountInfo: + return nil + default: + return err + } +} + func getExpectedTimelockVaultFromProtoAccount(authorityProto *commonpb.SolanaAccountId) (*common.Account, error) { authorityAccount, err := common.NewAccountFromProto(authorityProto) if err != nil { From eed91c1abbb6f9a7c1d5500ba32ce00b0cdeb7c8 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 29 Jan 2025 13:56:55 -0500 Subject: [PATCH 70/79] Fix Timelock oldest record metric --- pkg/code/async/geyser/backup.go | 11 ++++++----- pkg/code/async/geyser/metrics.go | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/code/async/geyser/backup.go b/pkg/code/async/geyser/backup.go index 364337da..eb159993 100644 --- a/pkg/code/async/geyser/backup.go +++ b/pkg/code/async/geyser/backup.go @@ -37,7 +37,7 @@ func (p *service) backupTimelockStateWorker(serviceCtx context.Context, interval delay := 0 * time.Second // Initially no delay, so we can run right after a deploy cursor := query.EmptyCursor - oldestRecord := time.Now() + oldestRecordTs := time.Now() for { select { case <-time.After(delay): @@ -58,11 +58,12 @@ func (p *service) backupTimelockStateWorker(serviceCtx context.Context, interval ) if err == timelock.ErrTimelockNotFound { p.metricStatusLock.Lock() - p.oldestTimelockRecord = &oldestRecord + copiedTs := oldestRecordTs + p.oldestTimelockRecord = &copiedTs p.metricStatusLock.Unlock() cursor = query.EmptyCursor - oldestRecord = time.Now() + oldestRecordTs = time.Now() return } else if err != nil { log.WithError(err).Warn("failed to get timelock records") @@ -73,8 +74,8 @@ func (p *service) backupTimelockStateWorker(serviceCtx context.Context, interval for _, timelockRecord := range timelockRecords { wg.Add(1) - if timelockRecord.LastUpdatedAt.Before(oldestRecord) { - oldestRecord = timelockRecord.LastUpdatedAt + if timelockRecord.LastUpdatedAt.Before(oldestRecordTs) { + oldestRecordTs = timelockRecord.LastUpdatedAt } go func(timelockRecord *timelock.Record) { diff --git a/pkg/code/async/geyser/metrics.go b/pkg/code/async/geyser/metrics.go index 739b182d..b90f169e 100644 --- a/pkg/code/async/geyser/metrics.go +++ b/pkg/code/async/geyser/metrics.go @@ -114,7 +114,8 @@ func (p *service) recordBackupWorkerStatusPollingEvent(ctx context.Context) { "is_active": p.backupTimelockStateWorkerStatus, } if p.oldestTimelockRecord != nil { - timelockMetrics["oldest_record_age"] = int(time.Since(*p.oldestTimelockRecord) / time.Second) + oldestRecordAgeSeconds := time.Since(*p.oldestTimelockRecord) / time.Second + timelockMetrics["oldest_record_age_s"] = int(oldestRecordAgeSeconds) p.oldestTimelockRecord = nil } metrics.RecordEvent(ctx, backupWorkerStatusEventName, timelockMetrics) From cff3ef8f07d333e8de89abce88b34ffe0c5e15f7 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 11 Feb 2025 08:48:29 -0500 Subject: [PATCH 71/79] Add support for VM airdrop virtual instruction --- pkg/solana/cvm/types_message.go | 24 ++++++++++++++++ pkg/solana/cvm/types_opcode.go | 2 ++ .../cvm/virtual_instructions_airdrop.go | 28 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 pkg/solana/cvm/virtual_instructions_airdrop.go diff --git a/pkg/solana/cvm/types_message.go b/pkg/solana/cvm/types_message.go index 80c2b191..eef0f19a 100644 --- a/pkg/solana/cvm/types_message.go +++ b/pkg/solana/cvm/types_message.go @@ -49,6 +49,30 @@ func GetCompactWithdrawMessage(args *GetCompactWithdrawMessageArgs) CompactMessa return hashMessage(message) } +type GetCompactAirdropMessageArgs struct { + Source ed25519.PublicKey + Destinations []ed25519.PublicKey + Amount uint64 + NonceAddress ed25519.PublicKey + NonceValue Hash +} + +func GetCompactAirdropMessage(args *GetCompactAirdropMessageArgs) CompactMessage { + amountBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(amountBytes, args.Amount) + + var message Message + message = append(message, []byte("airdrop")...) + message = append(message, args.Source...) + message = append(message, args.NonceAddress...) + message = append(message, args.NonceValue[:]...) + message = append(message, amountBytes...) + for _, destination := range args.Destinations { + message = append(message, destination...) + } + return hashMessage(message) +} + func hashMessage(msg Message) CompactMessage { h := sha256.New() h.Write(msg) diff --git a/pkg/solana/cvm/types_opcode.go b/pkg/solana/cvm/types_opcode.go index 35401792..93b0afa1 100644 --- a/pkg/solana/cvm/types_opcode.go +++ b/pkg/solana/cvm/types_opcode.go @@ -14,6 +14,8 @@ const ( OpcodeExternalRelay Opcode = 20 OpcodeConditionalTransfer Opcode = 12 + + OpcodeAirdrop Opcode = 30 ) func putOpcode(dst []byte, v Opcode, offset *int) { diff --git a/pkg/solana/cvm/virtual_instructions_airdrop.go b/pkg/solana/cvm/virtual_instructions_airdrop.go new file mode 100644 index 00000000..e6d0b6ab --- /dev/null +++ b/pkg/solana/cvm/virtual_instructions_airdrop.go @@ -0,0 +1,28 @@ +package cvm + +const ( + AirdropVirtrualInstructionDataSize = (SignatureSize + // signature + 8 + // amount + 1) // count +) + +type AirdropVirtualInstructionArgs struct { + Amount uint64 + Count uint8 + Signature Signature +} + +func NewAirdropVirtualInstruction( + args *AirdropVirtualInstructionArgs, +) VirtualInstruction { + var offset int + data := make([]byte, AirdropVirtrualInstructionDataSize) + putSignature(data, args.Signature, &offset) + putUint64(data, args.Amount, &offset) + putUint8(data, args.Count, &offset) + + return VirtualInstruction{ + Opcode: OpcodeAirdrop, + Data: data, + } +} From 7ccf638d86e0caabaacb2a4b7b035e8dd1a7449e Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 24 Feb 2025 10:18:00 -0500 Subject: [PATCH 72/79] Export MergeMemoryBanks utility --- pkg/code/transaction/transaction.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index 408e4cbd..a60dca03 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -101,7 +101,7 @@ func MakeInternalWithdrawTransaction( destinationMemory *common.Account, destinationIndex uint16, ) (solana.Transaction, error) { - mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) + mergedMemoryBanks, err := MergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) if err != nil { return solana.Transaction{}, err } @@ -145,7 +145,7 @@ func MakeExternalWithdrawTransaction( externalDestination *common.Account, ) (solana.Transaction, error) { - mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory) + mergedMemoryBanks, err := MergeMemoryBanks(nonceMemory, sourceMemory) if err != nil { return solana.Transaction{}, err } @@ -194,7 +194,7 @@ func MakeInternalTransferWithAuthorityTransaction( kinAmountInQuarks uint64, ) (solana.Transaction, error) { - mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) + mergedMemoryBanks, err := MergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) if err != nil { return solana.Transaction{}, err } @@ -240,7 +240,7 @@ func MakeExternalTransferWithAuthorityTransaction( externalDestination *common.Account, kinAmountInQuarks uint64, ) (solana.Transaction, error) { - mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory) + mergedMemoryBanks, err := MergeMemoryBanks(nonceMemory, sourceMemory) if err != nil { return solana.Transaction{}, err } @@ -294,7 +294,7 @@ func MakeInternalTreasuryAdvanceTransaction( treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) - mergedMemoryBanks, err := mergeMemoryBanks(accountMemory, relayMemory) + mergedMemoryBanks, err := MergeMemoryBanks(accountMemory, relayMemory) if err != nil { return solana.Transaction{}, err } @@ -401,7 +401,7 @@ func MakeCashChequeTransaction( treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) - mergedMemoryBanks, err := mergeMemoryBanks(nonceMemory, sourceMemory, relayMemory) + mergedMemoryBanks, err := MergeMemoryBanks(nonceMemory, sourceMemory, relayMemory) if err != nil { return solana.Transaction{}, err } @@ -434,7 +434,7 @@ func MakeCashChequeTransaction( return MakeNoncedTransaction(nonce, bh, execInstruction) } -type mergedMemoryBankResult struct { +type MergedMemoryBankResult struct { A *ed25519.PublicKey B *ed25519.PublicKey C *ed25519.PublicKey @@ -442,7 +442,7 @@ type mergedMemoryBankResult struct { Indices []uint8 } -func mergeMemoryBanks(accounts ...*common.Account) (*mergedMemoryBankResult, error) { +func MergeMemoryBanks(accounts ...*common.Account) (*MergedMemoryBankResult, error) { indices := make([]uint8, len(accounts)) orderedBanks := make([]*ed25519.PublicKey, 4) @@ -466,7 +466,7 @@ func mergeMemoryBanks(accounts ...*common.Account) (*mergedMemoryBankResult, err } } - return &mergedMemoryBankResult{ + return &MergedMemoryBankResult{ A: orderedBanks[0], B: orderedBanks[1], C: orderedBanks[2], From d6f495e3cd7217c1374b7ad7fefa6ca9fd4ad568 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 27 Feb 2025 09:03:46 -0500 Subject: [PATCH 73/79] Fix tests --- pkg/code/transaction/transaction_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/code/transaction/transaction_test.go b/pkg/code/transaction/transaction_test.go index 9011bb90..7371f069 100644 --- a/pkg/code/transaction/transaction_test.go +++ b/pkg/code/transaction/transaction_test.go @@ -84,7 +84,7 @@ func TestVmTransaction_MergedMemoryBanks_HappyPath(t *testing.T) { account2 := testutil.NewRandomAccount(t) account3 := testutil.NewRandomAccount(t) - res, err := mergeMemoryBanks(account1, account1, account2, account3, account2) + res, err := MergeMemoryBanks(account1, account1, account2, account3, account2) require.NoError(t, err) assert.EqualValues(t, account1.PublicKey().ToBytes(), *res.A) @@ -96,6 +96,6 @@ func TestVmTransaction_MergedMemoryBanks_HappyPath(t *testing.T) { } func TestVmTransaction_MergedMemoryBanks_TooManyAccounts(t *testing.T) { - _, err := mergeMemoryBanks(testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t)) + _, err := MergeMemoryBanks(testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t), testutil.NewRandomAccount(t)) assert.Error(t, err) } From 6edbcf88b599b5eb3555105d611cd0220f78f013 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 27 Feb 2025 12:59:14 -0500 Subject: [PATCH 74/79] Initial airdrop worker based on PoC --- pkg/code/async/airdrop/config.go | 41 +++++ pkg/code/async/airdrop/indexer.go | 36 ++++ pkg/code/async/airdrop/integration.go | 16 ++ pkg/code/async/airdrop/nonce.go | 34 ++++ pkg/code/async/airdrop/service.go | 122 +++++++++++++ pkg/code/async/airdrop/transaction.go | 242 ++++++++++++++++++++++++++ pkg/code/async/airdrop/worker.go | 63 +++++++ 7 files changed, 554 insertions(+) create mode 100644 pkg/code/async/airdrop/config.go create mode 100644 pkg/code/async/airdrop/indexer.go create mode 100644 pkg/code/async/airdrop/integration.go create mode 100644 pkg/code/async/airdrop/nonce.go create mode 100644 pkg/code/async/airdrop/service.go create mode 100644 pkg/code/async/airdrop/transaction.go create mode 100644 pkg/code/async/airdrop/worker.go diff --git a/pkg/code/async/airdrop/config.go b/pkg/code/async/airdrop/config.go new file mode 100644 index 00000000..0bfcc7c5 --- /dev/null +++ b/pkg/code/async/airdrop/config.go @@ -0,0 +1,41 @@ +package async_airdrop + +import ( + "github.com/code-payments/code-server/pkg/config" + "github.com/code-payments/code-server/pkg/config/env" +) + +// todo: setup configs + +const ( + envConfigPrefix = "AIRDROP_SERVICE_" + + DisableAirdropsConfigEnvName = envConfigPrefix + "DISABLE_AIRDROPS" + defaultDisableAirdrops = false + + AirdropperOwnerConfigEnvName = envConfigPrefix + "AIRDROPPER_OWNER" + defaultAirdropperOwner = "invalid" // ensure something valid is set + + NonceMemoryAccountConfigEnvName = envConfigPrefix + "NONCE_MEMORY_ACCOUNT" + defaultNonceMemoryAccount = "invalid" // ensure something valid is set +) + +type conf struct { + disableAirdrops config.Bool + airdropperOwner config.String + nonceMemoryAccount config.String +} + +// ConfigProvider defines how config values are pulled +type ConfigProvider func() *conf + +// WithEnvConfigs returns configuration pulled from environment variables +func WithEnvConfigs() ConfigProvider { + return func() *conf { + return &conf{ + disableAirdrops: env.NewBoolConfig(DisableAirdropsConfigEnvName, defaultDisableAirdrops), + airdropperOwner: env.NewStringConfig(AirdropperOwnerConfigEnvName, defaultAirdropperOwner), + nonceMemoryAccount: env.NewStringConfig(NonceMemoryAccountConfigEnvName, defaultNonceMemoryAccount), + } + } +} diff --git a/pkg/code/async/airdrop/indexer.go b/pkg/code/async/airdrop/indexer.go new file mode 100644 index 00000000..009da46e --- /dev/null +++ b/pkg/code/async/airdrop/indexer.go @@ -0,0 +1,36 @@ +package async_airdrop + +import ( + "context" + + "github.com/pkg/errors" + + indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" + + "github.com/code-payments/code-server/pkg/code/common" +) + +func (p *service) getVirtualTimelockAccountMemoryLocation(ctx context.Context, vm, owner *common.Account) (*common.Account, uint16, error) { + resp, err := p.vmIndexerClient.GetVirtualTimelockAccounts(ctx, &indexerpb.GetVirtualTimelockAccountsRequest{ + VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()}, + Owner: &indexerpb.Address{Value: owner.PublicKey().ToBytes()}, + }) + if err != nil { + return nil, 0, err + } else if resp.Result != indexerpb.GetVirtualTimelockAccountsResponse_OK { + return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) + } + + if len(resp.Items) > 1 { + return nil, 0, errors.New("multiple results returned") + } else if resp.Items[0].Storage.GetMemory() == nil { + return nil, 0, errors.New("account is compressed") + } + + protoMemory := resp.Items[0].Storage.GetMemory() + memory, err := common.NewAccountFromPublicKeyBytes(protoMemory.Account.Value) + if err != nil { + return nil, 0, err + } + return memory, uint16(protoMemory.Index), nil +} diff --git a/pkg/code/async/airdrop/integration.go b/pkg/code/async/airdrop/integration.go new file mode 100644 index 00000000..ef37d6c1 --- /dev/null +++ b/pkg/code/async/airdrop/integration.go @@ -0,0 +1,16 @@ +package async_airdrop + +import ( + "context" + + "github.com/code-payments/code-server/pkg/code/common" +) + +type Integration interface { + // GetOwnersToAirdropNow gets a set of owner accounts to airdrop right now, + // and the amount that should be airdropped. + GetOwnersToAirdropNow(ctx context.Context) ([]*common.Account, uint64, error) + + // OnSuccess is called when an airdrop completes + OnSuccess(ctx context.Context, owners ...*common.Account) error +} diff --git a/pkg/code/async/airdrop/nonce.go b/pkg/code/async/airdrop/nonce.go new file mode 100644 index 00000000..c061fbbc --- /dev/null +++ b/pkg/code/async/airdrop/nonce.go @@ -0,0 +1,34 @@ +package async_airdrop + +import ( + "context" + + "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +func (p *service) refreshNonceMemoryAccountState(ctx context.Context) error { + p.nonceMu.Lock() + defer p.nonceMu.Unlock() + + ai, err := p.data.GetBlockchainAccountInfo(ctx, p.nonceMemoryAccount.PublicKey().ToBase58(), solana.CommitmentFinalized) + if err != nil { + return err + } + return p.nonceMemoryAccountState.Unmarshal(ai.Data) +} + +func (p *service) getVdn() (*cvm.VirtualDurableNonce, uint16, error) { + p.nonceMu.Lock() + defer p.nonceMu.Unlock() + + vaData, _ := p.nonceMemoryAccountState.Data.Read(int(p.nextNonceIndex)) + var vdn cvm.VirtualDurableNonce + err := vdn.UnmarshalFromMemory(vaData) + if err != nil { + return nil, 0, err + } + index := p.nextNonceIndex + p.nextNonceIndex = (p.nextNonceIndex + 1) % uint16(len(p.nonceMemoryAccountState.Data.State)) + return &vdn, index, nil +} diff --git a/pkg/code/async/airdrop/service.go b/pkg/code/async/airdrop/service.go new file mode 100644 index 00000000..68472239 --- /dev/null +++ b/pkg/code/async/airdrop/service.go @@ -0,0 +1,122 @@ +package async_airdrop + +import ( + "context" + "sync" + "time" + + "github.com/sirupsen/logrus" + + indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" + + "github.com/code-payments/code-server/pkg/code/async" + "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +type service struct { + log *logrus.Entry + conf *conf + data code_data.Provider + vmIndexerClient indexerpb.IndexerClient + integration Integration + + airdropper *common.Account + airdropperTimelockAccounts *common.TimelockAccounts + airdropperMemoryAccount *common.Account + airdropperMemoryAccountIndex uint16 + + nonceMu sync.Mutex + nextNonceIndex uint16 + nonceMemoryAccount *common.Account + nonceMemoryAccountState *cvm.MemoryAccountWithData +} + +func New(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, integration Integration, configProvider ConfigProvider) (async.Service, error) { + ctx := context.Background() + + s := &service{ + log: logrus.StandardLogger().WithField("service", "airdrop"), + conf: configProvider(), + data: data, + vmIndexerClient: vmIndexerClient, + integration: integration, + } + + err := s.loadAirdropper(ctx) + if err != nil { + return nil, err + } + + err = s.loadNonceMemoryAccount(ctx) + if err != nil { + return nil, err + } + + return s, nil +} + +func (p *service) Start(ctx context.Context, interval time.Duration) error { + go func() { + err := p.airdropWorker(ctx, interval) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("airdrop processing loop terminated unexpectedly") + } + }() + + select { + case <-ctx.Done(): + return ctx.Err() + } +} + +func (p *service) loadAirdropper(ctx context.Context) error { + log := p.log.WithField("method", "loadAirdropper") + + vaultRecord, err := p.data.GetKey(ctx, p.conf.airdropperOwner.Get(ctx)) + if err != nil { + log.WithError(err).Warn("failed to load vault record") + return err + } + + p.airdropper, err = common.NewAccountFromPrivateKeyString(vaultRecord.PrivateKey) + if err != nil { + log.WithError(err).Warn("invalid private key") + return err + } + + p.airdropperTimelockAccounts, err = p.airdropper.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + log.WithError(err).Warn("failed to dervice timelock accounts") + return err + } + + p.airdropperMemoryAccount, p.airdropperMemoryAccountIndex, err = p.getVirtualTimelockAccountMemoryLocation(ctx, common.CodeVmAccount, p.airdropper) + if err != nil { + log.WithError(err).Warn("failed to get airdropper memory location") + return err + } + + return nil +} + +func (p *service) loadNonceMemoryAccount(ctx context.Context) (err error) { + log := p.log.WithField("method", "loadNonceMemoryAccount") + + p.nextNonceIndex = 0 + + p.nonceMemoryAccount, err = common.NewAccountFromPublicKeyString(p.conf.nonceMemoryAccount.Get(ctx)) + if err != nil { + log.WithError(err).Warn("invalid public key") + return err + } + + p.nonceMemoryAccountState = &cvm.MemoryAccountWithData{} + + err = p.refreshNonceMemoryAccountState(ctx) + if err != nil { + log.WithError(err).Warn("failed to refresh nonce memory account state") + } + return err +} diff --git a/pkg/code/async/airdrop/transaction.go b/pkg/code/async/airdrop/transaction.go new file mode 100644 index 00000000..2ba28ea8 --- /dev/null +++ b/pkg/code/async/airdrop/transaction.go @@ -0,0 +1,242 @@ +package async_airdrop + +import ( + "context" + "crypto/ed25519" + "math" + "time" + + "github.com/mr-tron/base58" + "github.com/pkg/errors" + + "github.com/code-payments/code-server/pkg/code/common" + "github.com/code-payments/code-server/pkg/code/data/deposit" + "github.com/code-payments/code-server/pkg/code/data/transaction" + transaction_util "github.com/code-payments/code-server/pkg/code/transaction" + "github.com/code-payments/code-server/pkg/currency" + "github.com/code-payments/code-server/pkg/solana" + compute_budget "github.com/code-payments/code-server/pkg/solana/computebudget" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +const ( + maxAirdropsInVixn = 50 + maxAirdropsInTxn = 200 + + cuLimitPerVixn = 200_000 + + txnWatchTimeout = 5 * time.Minute +) + +func (p *service) airdropToOwners(ctx context.Context, amount uint64, owners ...*common.Account) error { + type destinationInMemory struct { + owner *common.Account + memory *common.Account + index uint16 + } + + destinationsByMemory := make(map[string][]*destinationInMemory) + for _, owner := range owners { + memory, index, err := p.getVirtualTimelockAccountMemoryLocation(ctx, common.CodeVmAccount, owner) + if err != nil { + return err + } + + destinationsByMemory[memory.PublicKey().ToBase58()] = append(destinationsByMemory[memory.PublicKey().ToBase58()], &destinationInMemory{ + owner: owner, + memory: memory, + index: index, + }) + } + + for timelockMemoryAccount := range destinationsByMemory { + destinationsInMemory := destinationsByMemory[timelockMemoryAccount] + + var batchDestinations []*common.Account + var batchDestinationMemoryIndices []uint16 + for i, destinationInMemory := range destinationsInMemory { + batchDestinations = append(batchDestinations, destinationInMemory.owner) + batchDestinationMemoryIndices = append(batchDestinationMemoryIndices, destinationInMemory.index) + + if len(batchDestinations) >= maxAirdropsInTxn || i == len(destinationsInMemory)-1 { + sig, err := p.submitAirdrop(ctx, amount, batchDestinations, destinationsInMemory[0].memory, batchDestinationMemoryIndices) + if err != nil { + return err + } + + txn, err := p.watchTxn(ctx, sig) + if err != nil { + return err + } + + err = p.onSuccess(ctx, txn, amount, batchDestinations...) + if err != nil { + return err + } + + batchDestinations = nil + batchDestinationMemoryIndices = nil + } + } + } + + return nil +} + +func (p *service) submitAirdrop(ctx context.Context, amount uint64, destinations []*common.Account, destinationMemoryAccount *common.Account, destinationMemoryIndices []uint16) (string, error) { + if len(destinations) == 0 { + return "", errors.New("no destinations") + } + if len(destinations) > maxAirdropsInTxn { + return "", errors.New("too many destinations") + } + + ixnsNeeded := int(math.Ceil(float64(len(destinations)) / float64(maxAirdropsInVixn))) + cuLimit := uint32(cuLimitPerVixn * ixnsNeeded) + + ixns := []solana.Instruction{ + compute_budget.SetComputeUnitPrice(1_000), // todo: dynamic + compute_budget.SetComputeUnitLimit(cuLimit), + } + + var batchPublicKeys []ed25519.PublicKey + var batchMemoryIndices []uint16 + batchMemoryAccounts := []*common.Account{p.nonceMemoryAccount, p.airdropperMemoryAccount} + for i, destination := range destinations { + batchPublicKeys = append(batchPublicKeys, destination.PublicKey().ToBytes()) + batchMemoryIndices = append(batchMemoryIndices, destinationMemoryIndices[i]) + batchMemoryAccounts = append(batchMemoryAccounts, destinationMemoryAccount) + + if len(batchPublicKeys) >= maxAirdropsInVixn || i == len(destinations)-1 { + vdn, nonceIndex, err := p.getVdn() + if err != nil { + return "", err + } + + msg := cvm.GetCompactAirdropMessage(&cvm.GetCompactAirdropMessageArgs{ + Source: p.airdropperTimelockAccounts.Vault.PublicKey().ToBytes(), + Destinations: batchPublicKeys, + Amount: amount, + NonceAddress: vdn.Address, + NonceValue: vdn.Value, + }) + + vsig, err := p.airdropper.Sign(msg[:]) + if err != nil { + return "", err + } + + vixn := cvm.NewAirdropVirtualInstruction( + &cvm.AirdropVirtualInstructionArgs{ + Amount: amount, + Count: uint8(len(batchPublicKeys)), + Signature: cvm.Signature(vsig), + }, + ) + + mergedMemoryBanks, err := transaction_util.MergeMemoryBanks(batchMemoryAccounts...) + if err != nil { + return "", err + } + + ixns = append(ixns, cvm.NewExecInstruction( + &cvm.ExecInstructionAccounts{ + VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), + Vm: common.CodeVmAccount.PublicKey().ToBytes(), + VmMemA: mergedMemoryBanks.A, + VmMemB: mergedMemoryBanks.B, + VmMemC: mergedMemoryBanks.C, + }, + &cvm.ExecInstructionArgs{ + Opcode: vixn.Opcode, + MemIndices: append([]uint16{nonceIndex, p.airdropperMemoryAccountIndex}, batchMemoryIndices...), + MemBanks: mergedMemoryBanks.Indices, + Data: vixn.Data, + }, + )) + + batchPublicKeys = nil + batchMemoryIndices = nil + batchMemoryAccounts = []*common.Account{p.nonceMemoryAccount, p.airdropperMemoryAccount} + } + } + + txn := solana.NewTransaction(common.GetSubsidizer().PublicKey().ToBytes(), ixns...) + + bh, err := p.data.GetBlockchainLatestBlockhash(ctx) + if err != nil { + return "", err + } + txn.SetBlockhash(bh) + + err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) + if err != nil { + return "", err + } + + sig, err := p.data.SubmitBlockchainTransaction(ctx, &txn) + if err != nil { + return "", err + } + + return base58.Encode(sig[:]), nil +} + +func (p *service) watchTxn(ctx context.Context, sig string) (*solana.ConfirmedTransaction, error) { + for range txnWatchTimeout / time.Second { + time.Sleep(time.Second) + + txn, err := p.data.GetBlockchainTransaction(ctx, sig, solana.CommitmentFinalized) + if err != nil { + continue + } + + if txn.Err != nil || txn.Meta.Err != nil { + return nil, errors.New("transaction failed") + } + + return txn, nil + } + + return nil, errors.New("transaction didn't finalize") +} + +func (p *service) onSuccess(ctx context.Context, txn *solana.ConfirmedTransaction, amount uint64, owners ...*common.Account) error { + var usdMarketValue float64 + usdExchangeRateRecord, err := p.data.GetExchangeRate(ctx, currency.USD, *txn.BlockTime) + if err == nil { + usdMarketValue = usdExchangeRateRecord.Rate * float64(amount) + } else { + // todo: warn and move on + } + + var vaults []*common.Account + for _, owner := range owners { + timelockAccounts, err := owner.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + if err != nil { + return err + } + vaults = append(vaults, timelockAccounts.Vault) + } + + for _, vault := range vaults { + externalDepositRecord := &deposit.Record{ + Signature: base58.Encode(txn.Transaction.Signature()), + Destination: vault.PublicKey().ToBase58(), + Amount: amount, + UsdMarketValue: usdMarketValue, + + Slot: txn.Slot, + ConfirmationState: transaction.ConfirmationFinalized, + + CreatedAt: time.Now(), + } + + err := p.data.SaveExternalDeposit(ctx, externalDepositRecord) + if err != nil { + return err + } + } + + return p.integration.OnSuccess(ctx, owners...) +} diff --git a/pkg/code/async/airdrop/worker.go b/pkg/code/async/airdrop/worker.go new file mode 100644 index 00000000..89ad4c5b --- /dev/null +++ b/pkg/code/async/airdrop/worker.go @@ -0,0 +1,63 @@ +package async_airdrop + +import ( + "context" + "time" + + "github.com/newrelic/go-agent/v3/newrelic" + + "github.com/code-payments/code-server/pkg/metrics" +) + +func (p *service) airdropWorker(serviceCtx context.Context, interval time.Duration) error { + delay := time.After(interval) + + for { + select { + case <-delay: + nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) + m := nr.StartTransaction("async__airdrop_service__airdrop") + defer m.End() + tracedCtx := newrelic.NewContext(serviceCtx, m) + + start := time.Now() + + err := p.doAirdrop(tracedCtx) + if err != nil { + m.NoticeError(err) + } + + delay = time.After(interval - time.Since(start)) + case <-serviceCtx.Done(): + return serviceCtx.Err() + } + } +} + +func (p *service) doAirdrop(ctx context.Context) error { + log := p.log.WithField("method", "doAirdrop") + + err := p.refreshNonceMemoryAccountState(ctx) + if err != nil { + log.WithError(err).Warn("failed to refresh nonce memory account state") + return err + } + + owners, amount, err := p.integration.GetOwnersToAirdropNow(ctx) + if err != nil { + log.WithError(err).Warn("failed to get owners to airdrop to") + return err + } + + if len(owners) == 0 { + return nil + } + + err = p.airdropToOwners(ctx, amount, owners...) + if err != nil { + log.WithError(err).Warn("failed to airdrop to owners") + return err + } + + return nil +} From 8a8a200e3475d2b3fac85452cb3a4235d9ea0787 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 3 Mar 2025 09:05:06 -0500 Subject: [PATCH 75/79] Skip airdrops for virtual accounts that are not currently indexed --- pkg/code/async/airdrop/indexer.go | 6 ++++++ pkg/code/async/airdrop/transaction.go | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/code/async/airdrop/indexer.go b/pkg/code/async/airdrop/indexer.go index 009da46e..224ca75b 100644 --- a/pkg/code/async/airdrop/indexer.go +++ b/pkg/code/async/airdrop/indexer.go @@ -10,6 +10,10 @@ import ( "github.com/code-payments/code-server/pkg/code/common" ) +var ( + errNotIndexed = errors.New("virtual account is not indexed") +) + func (p *service) getVirtualTimelockAccountMemoryLocation(ctx context.Context, vm, owner *common.Account) (*common.Account, uint16, error) { resp, err := p.vmIndexerClient.GetVirtualTimelockAccounts(ctx, &indexerpb.GetVirtualTimelockAccountsRequest{ VmAccount: &indexerpb.Address{Value: vm.PublicKey().ToBytes()}, @@ -17,6 +21,8 @@ func (p *service) getVirtualTimelockAccountMemoryLocation(ctx context.Context, v }) if err != nil { return nil, 0, err + } else if resp.Result == indexerpb.GetVirtualTimelockAccountsResponse_NOT_FOUND { + return nil, 0, errNotIndexed } else if resp.Result != indexerpb.GetVirtualTimelockAccountsResponse_OK { return nil, 0, errors.Errorf("received rpc result %s", resp.Result.String()) } diff --git a/pkg/code/async/airdrop/transaction.go b/pkg/code/async/airdrop/transaction.go index 2ba28ea8..4dd1718d 100644 --- a/pkg/code/async/airdrop/transaction.go +++ b/pkg/code/async/airdrop/transaction.go @@ -38,7 +38,9 @@ func (p *service) airdropToOwners(ctx context.Context, amount uint64, owners ... destinationsByMemory := make(map[string][]*destinationInMemory) for _, owner := range owners { memory, index, err := p.getVirtualTimelockAccountMemoryLocation(ctx, common.CodeVmAccount, owner) - if err != nil { + if err == errNotIndexed { + continue + } else if err != nil { return err } From afa66f77107c77262c746ac7b7e5fb1468f04394 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 3 Mar 2025 12:53:03 -0500 Subject: [PATCH 76/79] Be more resilient against missing exchange rates for airdrops --- pkg/code/async/airdrop/transaction.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/code/async/airdrop/transaction.go b/pkg/code/async/airdrop/transaction.go index 4dd1718d..bc415b2e 100644 --- a/pkg/code/async/airdrop/transaction.go +++ b/pkg/code/async/airdrop/transaction.go @@ -204,7 +204,7 @@ func (p *service) watchTxn(ctx context.Context, sig string) (*solana.ConfirmedTr } func (p *service) onSuccess(ctx context.Context, txn *solana.ConfirmedTransaction, amount uint64, owners ...*common.Account) error { - var usdMarketValue float64 + usdMarketValue := 0.001 usdExchangeRateRecord, err := p.data.GetExchangeRate(ctx, currency.USD, *txn.BlockTime) if err == nil { usdMarketValue = usdExchangeRateRecord.Rate * float64(amount) From 8bc9d9a4512ff49e6c0ac44023f8397357f993c6 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 7 Mar 2025 11:29:19 -0500 Subject: [PATCH 77/79] Reduce maxAirdropsInTxn to 150 --- pkg/code/async/airdrop/transaction.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/code/async/airdrop/transaction.go b/pkg/code/async/airdrop/transaction.go index bc415b2e..12558221 100644 --- a/pkg/code/async/airdrop/transaction.go +++ b/pkg/code/async/airdrop/transaction.go @@ -21,7 +21,7 @@ import ( const ( maxAirdropsInVixn = 50 - maxAirdropsInTxn = 200 + maxAirdropsInTxn = 150 cuLimitPerVixn = 200_000 From 362c153aaa7847c41d25be160f8bd8fa1f1fcd04 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Wed, 2 Apr 2025 14:50:42 -0400 Subject: [PATCH 78/79] Cleanup VM branch (#191) --- go.mod | 57 +- go.sum | 320 +- pkg/cluster/grpc/example/cluster.go | 4 + pkg/cluster/grpc/example/cluster_test.go | 2 + pkg/code/antispam/address.go | 11 - pkg/code/antispam/airdrop.go | 235 +- pkg/code/antispam/config.go | 216 -- pkg/code/antispam/guard.go | 36 +- pkg/code/antispam/guard_test.go | 945 ------ pkg/code/antispam/intent.go | 490 +-- pkg/code/antispam/ip.go | 11 - pkg/code/antispam/limiter.go | 24 - pkg/code/antispam/metrics.go | 12 +- pkg/code/antispam/phone.go | 30 - pkg/code/antispam/phone_verification.go | 187 -- pkg/code/antispam/reason.go | 11 - pkg/code/antispam/swap.go | 62 - pkg/code/async/account/gift_card.go | 154 +- pkg/code/async/account/gift_card_test.go | 13 +- pkg/code/async/account/service.go | 32 +- pkg/code/async/account/swap.go | 2 + pkg/code/async/account/testutil.go | 15 +- pkg/code/async/airdrop/service.go | 2 +- pkg/code/async/airdrop/transaction.go | 2 +- pkg/code/async/commitment/merkle_tree.go | 70 - pkg/code/async/commitment/metrics.go | 48 - pkg/code/async/commitment/service.go | 55 - .../async/commitment/temporary_privacy.go | 91 - .../commitment/temporary_privacy_test.go | 50 - pkg/code/async/commitment/testutil.go | 289 -- pkg/code/async/commitment/transaction.go | 47 - pkg/code/async/commitment/util.go | 61 - pkg/code/async/commitment/worker.go | 253 -- pkg/code/async/commitment/worker_test.go | 3 - pkg/code/async/geyser/backup.go | 49 - pkg/code/async/geyser/external_deposit.go | 34 +- pkg/code/async/geyser/handler.go | 44 +- pkg/code/async/geyser/handler_test.go | 2 +- pkg/code/async/geyser/messenger.go | 278 -- pkg/code/async/geyser/service.go | 13 +- pkg/code/async/geyser/timelock.go | 2 +- pkg/code/async/sequencer/action_handler.go | 66 - pkg/code/async/sequencer/commitment.go | 79 - .../async/sequencer/fulfillment_handler.go | 997 +----- pkg/code/async/sequencer/intent_handler.go | 352 +- pkg/code/async/sequencer/payment.go | 77 - pkg/code/async/treasury/config.go | 60 - pkg/code/async/treasury/merkle_tree.go | 458 --- pkg/code/async/treasury/merkle_tree_test.go | 113 - pkg/code/async/treasury/metrics.go | 84 - pkg/code/async/treasury/recent_root.go | 388 --- pkg/code/async/treasury/recent_root_test.go | 138 - pkg/code/async/treasury/service.go | 58 - pkg/code/async/treasury/testutil.go | 462 --- pkg/code/async/treasury/worker.go | 127 - pkg/code/async/user/service.go | 54 - pkg/code/async/user/twitter.go | 409 --- pkg/code/async/webhook/service.go | 2 +- pkg/code/async/webhook/worker_test.go | 4 +- pkg/code/auth/signature.go | 35 - pkg/code/auth/signature_test.go | 64 - pkg/code/balance/calculator_test.go | 64 +- pkg/code/chat/chat.go | 60 - pkg/code/chat/message_blockchain.go | 32 - pkg/code/chat/message_cash_transactions.go | 167 - pkg/code/chat/message_code_team.go | 70 - pkg/code/chat/message_kin_purchases.go | 102 - pkg/code/chat/message_merchant.go | 187 -- pkg/code/chat/message_tips.go | 93 - pkg/code/chat/sender.go | 117 - pkg/code/chat/sender_test.go | 239 -- pkg/code/chat/util.go | 198 -- pkg/code/chat/util_test.go | 115 - pkg/code/common/account.go | 120 +- pkg/code/common/account_test.go | 165 - pkg/code/common/mint.go | 54 +- pkg/code/common/mint_test.go | 37 + pkg/code/common/owner.go | 18 +- pkg/code/common/owner_test.go | 41 +- pkg/code/common/subsidizer.go | 25 +- pkg/code/common/subsidizer_test.go | 12 +- pkg/code/common/vm.go | 14 +- pkg/code/config/config.go | 38 + pkg/code/data/action/action.go | 14 +- pkg/code/data/action/memory/store.go | 2 +- pkg/code/data/action/postgres/model.go | 95 +- pkg/code/data/action/postgres/store.go | 2 +- pkg/code/data/action/postgres/store_test.go | 2 - pkg/code/data/action/tests/tests.go | 6 +- pkg/code/data/airdrop/memory/store.go | 52 - pkg/code/data/airdrop/memory/store_test.go | 15 - pkg/code/data/airdrop/postgres/model.go | 80 - pkg/code/data/airdrop/postgres/store.go | 42 - pkg/code/data/airdrop/postgres/store_test.go | 108 - pkg/code/data/airdrop/store.go | 11 - pkg/code/data/airdrop/tests/tests.go | 38 - pkg/code/data/badgecount/badge_count.go | 46 - pkg/code/data/badgecount/memory/store.go | 91 - pkg/code/data/badgecount/memory/store_test.go | 15 - pkg/code/data/badgecount/postgres/model.go | 107 - pkg/code/data/badgecount/postgres/store.go | 40 - .../data/badgecount/postgres/store_test.go | 109 - pkg/code/data/badgecount/store.go | 21 - pkg/code/data/badgecount/tests/tests.go | 77 - pkg/code/data/blockchain.go | 6 +- pkg/code/data/chat/memory/store.go | 440 --- pkg/code/data/chat/memory/store_test.go | 15 - pkg/code/data/chat/model.go | 203 -- pkg/code/data/chat/model_test.go | 22 - pkg/code/data/chat/postgres/model.go | 413 --- pkg/code/data/chat/postgres/store.go | 129 - pkg/code/data/chat/postgres/store_test.go | 134 - pkg/code/data/chat/store.go | 56 - pkg/code/data/chat/tests/tests.go | 445 --- pkg/code/data/commitment/commitment.go | 173 - pkg/code/data/commitment/memory/store.go | 359 -- pkg/code/data/commitment/memory/store_test.go | 15 - pkg/code/data/commitment/postgres/model.go | 328 -- pkg/code/data/commitment/postgres/store.go | 117 - .../data/commitment/postgres/store_test.go | 128 - pkg/code/data/commitment/store.go | 49 - pkg/code/data/commitment/tests/tests.go | 432 --- pkg/code/data/contact/memory/store.go | 121 - pkg/code/data/contact/memory/store_test.go | 16 - pkg/code/data/contact/postgres/model.go | 156 - pkg/code/data/contact/postgres/store.go | 82 - pkg/code/data/contact/postgres/store_test.go | 111 - pkg/code/data/contact/store.go | 28 - pkg/code/data/contact/tests/tests.go | 128 - pkg/code/data/event/memory/store.go | 102 - pkg/code/data/event/memory/store_test.go | 16 - pkg/code/data/event/postgres/model.go | 193 -- pkg/code/data/event/postgres/store.go | 46 - pkg/code/data/event/postgres/store_test.go | 124 - pkg/code/data/event/record.go | 180 - pkg/code/data/event/store.go | 21 - pkg/code/data/event/tests/tests.go | 109 - pkg/code/data/external.go | 33 +- pkg/code/data/external_test.go | 12 +- pkg/code/data/fulfillment/fulfillment.go | 32 +- pkg/code/data/fulfillment/memory/store.go | 37 - pkg/code/data/fulfillment/postgres/model.go | 76 +- pkg/code/data/fulfillment/postgres/store.go | 5 - .../data/fulfillment/postgres/store_test.go | 2 - pkg/code/data/fulfillment/store.go | 5 - pkg/code/data/fulfillment/tests/tests.go | 133 - pkg/code/data/intent/intent.go | 587 +--- pkg/code/data/intent/memory/store.go | 385 +-- pkg/code/data/intent/postgres/model.go | 423 +-- pkg/code/data/intent/postgres/store.go | 89 +- pkg/code/data/intent/postgres/store_test.go | 13 +- pkg/code/data/intent/store.go | 32 - pkg/code/data/intent/tests/tests.go | 876 +---- pkg/code/data/internal.go | 696 +--- pkg/code/data/invite/v2/memory/store.go | 191 -- pkg/code/data/invite/v2/memory/store_test.go | 16 - pkg/code/data/invite/v2/postgres/model.go | 290 -- .../data/invite/v2/postgres/model_test.go | 59 - pkg/code/data/invite/v2/postgres/store.go | 109 - .../data/invite/v2/postgres/store_test.go | 125 - pkg/code/data/invite/v2/store.go | 131 - pkg/code/data/invite/v2/tests/tests.go | 526 --- pkg/code/data/login/login.go | 65 - pkg/code/data/login/memory/store.go | 121 - pkg/code/data/login/memory/store_test.go | 15 - pkg/code/data/login/postgres/model.go | 152 - pkg/code/data/login/postgres/store.go | 82 - pkg/code/data/login/postgres/store_test.go | 109 - pkg/code/data/login/store.go | 23 - pkg/code/data/login/tests/tests.go | 142 - pkg/code/data/payment/memory/store.go | 357 -- pkg/code/data/payment/memory/store_test.go | 15 - pkg/code/data/payment/payment.go | 105 - pkg/code/data/payment/postgres/model.go | 362 -- pkg/code/data/payment/postgres/store.go | 125 - pkg/code/data/payment/postgres/store_test.go | 125 - pkg/code/data/payment/store.go | 68 - pkg/code/data/payment/tests/tests.go | 148 - pkg/code/data/paymentrequest/tests/tests.go | 5 +- pkg/code/data/paywall/memory/store.go | 90 - pkg/code/data/paywall/memory/store_test.go | 15 - pkg/code/data/paywall/postgres/model.go | 111 - pkg/code/data/paywall/postgres/store.go | 47 - pkg/code/data/paywall/postgres/store_test.go | 113 - pkg/code/data/paywall/record.go | 90 - pkg/code/data/paywall/store.go | 17 - pkg/code/data/paywall/tests/tests.go | 65 - pkg/code/data/phone/memory/store.go | 390 --- pkg/code/data/phone/memory/stores_test.go | 15 - pkg/code/data/phone/postgres/model.go | 509 --- pkg/code/data/phone/postgres/model_test.go | 97 - pkg/code/data/phone/postgres/store.go | 161 - pkg/code/data/phone/postgres/store_test.go | 150 - pkg/code/data/phone/store.go | 270 -- pkg/code/data/phone/tests/tests.go | 511 --- pkg/code/data/preferences/memory/store.go | 92 - .../data/preferences/memory/store_test.go | 15 - pkg/code/data/preferences/postgres/model.go | 100 - pkg/code/data/preferences/postgres/store.go | 53 - .../data/preferences/postgres/store_test.go | 109 - pkg/code/data/preferences/preferences.go | 66 - pkg/code/data/preferences/store.go | 21 - pkg/code/data/preferences/tests/tests.go | 67 - pkg/code/data/push/memory/store.go | 138 - pkg/code/data/push/memory/store_test.go | 15 - pkg/code/data/push/postgres/model.go | 121 - pkg/code/data/push/postgres/store.go | 70 - pkg/code/data/push/postgres/store_test.go | 113 - pkg/code/data/push/push_token.go | 78 - pkg/code/data/push/store.go | 28 - pkg/code/data/push/tests/tests.go | 177 - pkg/code/data/transaction/transaction.go | 6 +- pkg/code/data/treasury/memory/store.go | 278 -- pkg/code/data/treasury/memory/store_test.go | 15 - pkg/code/data/treasury/postgres/model.go | 373 --- pkg/code/data/treasury/postgres/store.go | 105 - pkg/code/data/treasury/postgres/store_test.go | 153 - pkg/code/data/treasury/store.go | 38 - pkg/code/data/treasury/tests/tests.go | 263 -- pkg/code/data/treasury/treasury.go | 308 -- pkg/code/data/treasury/treasury_test.go | 46 - pkg/code/data/twitter/memory/store.go | 217 -- pkg/code/data/twitter/memory/store_test.go | 15 - pkg/code/data/twitter/postgres/model.go | 222 -- pkg/code/data/twitter/postgres/store.go | 87 - pkg/code/data/twitter/postgres/store_test.go | 136 - pkg/code/data/twitter/store.go | 39 - pkg/code/data/twitter/tests/tests.go | 205 -- pkg/code/data/twitter/user.go | 75 - pkg/code/data/user/identity/memory/store.go | 96 - .../data/user/identity/memory/store_test.go | 15 - pkg/code/data/user/identity/postgres/model.go | 107 - .../data/user/identity/postgres/model_test.go | 44 - pkg/code/data/user/identity/postgres/store.go | 49 - .../data/user/identity/postgres/store_test.go | 110 - pkg/code/data/user/identity/store.go | 60 - pkg/code/data/user/identity/tests/tests.go | 70 - pkg/code/data/user/model.go | 165 - pkg/code/data/user/model_test.go | 61 - pkg/code/data/user/storage/memory/store.go | 102 - .../data/user/storage/memory/store_test.go | 15 - pkg/code/data/user/storage/postgres/model.go | 101 - .../data/user/storage/postgres/model_test.go | 47 - pkg/code/data/user/storage/postgres/store.go | 49 - .../data/user/storage/postgres/store_test.go | 109 - pkg/code/data/user/storage/store.go | 68 - pkg/code/data/user/storage/tests/tests.go | 73 - pkg/code/event/client.go | 52 - pkg/code/exchangerate/validation.go | 20 +- .../lawenforcement/anti_money_laundering.go | 152 +- .../anti_money_laundering_test.go | 2 + pkg/code/limit/limits.go | 655 ++-- pkg/code/localization/currency.go | 236 -- pkg/code/localization/device.go | 37 - pkg/code/localization/keys.go | 191 -- pkg/code/localization/util.go | 30 - pkg/code/push/badge_count.go | 101 - pkg/code/push/data.go | 140 - pkg/code/push/notifications.go | 405 --- pkg/code/push/text.go | 60 - pkg/code/push/util.go | 41 - pkg/code/server/{grpc => }/account/server.go | 32 +- .../server/{grpc => }/account/server_test.go | 78 +- .../server/{grpc => }/currency/currency.go | 0 pkg/code/server/grpc/badge/server.go | 70 - pkg/code/server/grpc/badge/server_test.go | 165 - pkg/code/server/grpc/chat/server.go | 531 --- pkg/code/server/grpc/chat/server_test.go | 946 ------ pkg/code/server/grpc/contact/server.go | 250 -- pkg/code/server/grpc/contact/server_test.go | 499 --- pkg/code/server/grpc/device/server.go | 134 - pkg/code/server/grpc/device/server_test.go | 192 -- pkg/code/server/grpc/invite/v2/server.go | 54 - pkg/code/server/grpc/micropayment/server.go | 317 -- pkg/code/server/grpc/phone/server.go | 443 --- pkg/code/server/grpc/phone/server_test.go | 639 ---- pkg/code/server/grpc/push/server.go | 124 - pkg/code/server/grpc/push/server_test.go | 381 --- .../grpc/transaction/v2/action_handler.go | 877 ----- pkg/code/server/grpc/transaction/v2/config.go | 165 - .../grpc/transaction/v2/intent_handler.go | 2914 ----------------- pkg/code/server/grpc/transaction/v2/limits.go | 223 -- pkg/code/server/grpc/transaction/v2/proof.go | 271 -- .../server/grpc/transaction/v2/treasury.go | 248 -- pkg/code/server/grpc/user/config.go | 42 - pkg/code/server/grpc/user/limiter.go | 74 - pkg/code/server/grpc/user/server.go | 741 ----- pkg/code/server/grpc/user/server_test.go | 1698 ---------- .../server/{grpc => }/messaging/client.go | 0 .../server/{grpc => }/messaging/config.go | 0 pkg/code/server/{grpc => }/messaging/error.go | 0 .../server/{grpc => }/messaging/internal.go | 0 .../{grpc => }/messaging/message_handler.go | 168 +- .../server/{grpc => }/messaging/server.go | 17 +- .../{grpc => }/messaging/server_test.go | 147 +- .../server/{grpc => }/messaging/stream.go | 0 .../server/{grpc => }/messaging/testutil.go | 117 +- pkg/code/server/micropayment/server.go | 169 + .../{grpc => }/micropayment/server_test.go | 253 +- pkg/code/server/transaction/action_handler.go | 448 +++ .../transaction/v2 => transaction}/airdrop.go | 83 +- .../v2 => transaction}/airdrop_test.go | 0 pkg/code/server/transaction/config.go | 116 + .../transaction/v2 => transaction}/errors.go | 42 +- .../transaction/v2 => transaction}/intent.go | 459 +-- pkg/code/server/transaction/intent_handler.go | 1428 ++++++++ .../v2 => transaction}/intent_test.go | 0 pkg/code/server/transaction/limits.go | 66 + .../v2 => transaction}/limits_test.go | 0 .../v2 => transaction}/local_simulation.go | 96 +- .../local_simulation_test.go | 0 .../transaction/v2 => transaction}/metrics.go | 0 .../transaction/v2 => transaction}/onramp.go | 16 +- .../v2 => transaction}/onramp_test.go | 0 .../transaction/v2 => transaction}/server.go | 45 +- .../transaction/v2 => transaction}/swap.go | 78 +- .../v2 => transaction}/testutil.go | 0 pkg/code/server/web/request/client.go | 130 - pkg/code/server/web/request/model.go | 163 - pkg/code/server/web/request/server.go | 150 - pkg/code/server/web/request/util.go | 63 - pkg/code/thirdparty/message.go | 39 - pkg/code/transaction/instruction.go | 31 +- pkg/code/transaction/memo.go | 41 - pkg/code/transaction/transaction.go | 160 - pkg/code/webhook/execution.go | 4 +- pkg/code/webhook/execution_test.go | 33 +- pkg/code/webhook/payload.go | 7 - pkg/currency/iso.go | 344 +- pkg/device/android/verifier.go | 143 - pkg/device/composite/verifier.go | 86 - pkg/device/ios/verifier.go | 142 - pkg/device/memory/verifier.go | 52 - pkg/device/verifier.go | 14 - .../metrics/new_relic_server_interceptor.go | 68 +- pkg/kikcode/currency.go | 169 - pkg/kikcode/encoding/Array.h | 170 - pkg/kikcode/encoding/Counted.h | 140 - pkg/kikcode/encoding/Exception.cpp | 43 - pkg/kikcode/encoding/Exception.h | 51 - pkg/kikcode/encoding/GenericGF.cpp | 150 - pkg/kikcode/encoding/GenericGF.h | 73 - pkg/kikcode/encoding/GenericGFPoly.cpp | 221 -- pkg/kikcode/encoding/GenericGFPoly.h | 56 - .../encoding/IllegalArgumentException.cpp | 27 - .../encoding/IllegalArgumentException.h | 36 - pkg/kikcode/encoding/IllegalStateException.h | 35 - pkg/kikcode/encoding/ReaderException.h | 37 - pkg/kikcode/encoding/ReedSolomonDecoder.cpp | 174 - pkg/kikcode/encoding/ReedSolomonDecoder.h | 49 - pkg/kikcode/encoding/ReedSolomonEncoder.cpp | 81 - pkg/kikcode/encoding/ReedSolomonEncoder.h | 25 - pkg/kikcode/encoding/ReedSolomonException.cpp | 30 - pkg/kikcode/encoding/ReedSolomonException.h | 33 - pkg/kikcode/encoding/ZXing.h | 133 - pkg/kikcode/encoding/encoding.go | 49 - pkg/kikcode/encoding/encoding_test.go | 24 - pkg/kikcode/encoding/kikcode_constants.h | 6 - pkg/kikcode/encoding/kikcode_encoding.cpp | 472 --- pkg/kikcode/encoding/kikcode_encoding.h | 125 - pkg/kikcode/encoding/kikcode_wrapper.cpp | 50 - pkg/kikcode/encoding/kikcode_wrapper.h | 17 - pkg/kikcode/encoding/kikcodes.cpp | 96 - pkg/kikcode/encoding/kikcodes.h | 53 - pkg/kikcode/payload.go | 208 -- pkg/kikcode/qr.go | 179 - pkg/kikcode/rendezvous.go | 11 - pkg/kin/kin.go | 23 - pkg/kin/memo.go | 183 -- pkg/kin/memo_test.go | 165 - pkg/kin/utils.go | 69 - pkg/kin/utils_test.go | 76 - pkg/netutil/ip.go | 46 - pkg/phone/mcc.go | 268 -- pkg/phone/memory/verifier.go | 128 - pkg/phone/metadata.go | 39 - pkg/phone/twilio/verifier.go | 431 --- pkg/phone/validation.go | 22 - pkg/phone/verifier.go | 51 - pkg/push/fcm/provider.go | 163 - pkg/push/memory/provider.go | 63 - pkg/push/provider.go | 28 - pkg/twitter/client.go | 405 --- 383 files changed, 3717 insertions(+), 51979 deletions(-) delete mode 100644 pkg/code/antispam/address.go delete mode 100644 pkg/code/antispam/config.go delete mode 100644 pkg/code/antispam/guard_test.go delete mode 100644 pkg/code/antispam/ip.go delete mode 100644 pkg/code/antispam/limiter.go delete mode 100644 pkg/code/antispam/phone.go delete mode 100644 pkg/code/antispam/phone_verification.go delete mode 100644 pkg/code/antispam/reason.go delete mode 100644 pkg/code/async/commitment/merkle_tree.go delete mode 100644 pkg/code/async/commitment/metrics.go delete mode 100644 pkg/code/async/commitment/service.go delete mode 100644 pkg/code/async/commitment/temporary_privacy.go delete mode 100644 pkg/code/async/commitment/temporary_privacy_test.go delete mode 100644 pkg/code/async/commitment/testutil.go delete mode 100644 pkg/code/async/commitment/transaction.go delete mode 100644 pkg/code/async/commitment/util.go delete mode 100644 pkg/code/async/commitment/worker.go delete mode 100644 pkg/code/async/commitment/worker_test.go delete mode 100644 pkg/code/async/geyser/messenger.go delete mode 100644 pkg/code/async/sequencer/commitment.go delete mode 100644 pkg/code/async/sequencer/payment.go delete mode 100644 pkg/code/async/treasury/config.go delete mode 100644 pkg/code/async/treasury/merkle_tree.go delete mode 100644 pkg/code/async/treasury/merkle_tree_test.go delete mode 100644 pkg/code/async/treasury/metrics.go delete mode 100644 pkg/code/async/treasury/recent_root.go delete mode 100644 pkg/code/async/treasury/recent_root_test.go delete mode 100644 pkg/code/async/treasury/service.go delete mode 100644 pkg/code/async/treasury/testutil.go delete mode 100644 pkg/code/async/treasury/worker.go delete mode 100644 pkg/code/async/user/service.go delete mode 100644 pkg/code/async/user/twitter.go delete mode 100644 pkg/code/chat/chat.go delete mode 100644 pkg/code/chat/message_blockchain.go delete mode 100644 pkg/code/chat/message_cash_transactions.go delete mode 100644 pkg/code/chat/message_code_team.go delete mode 100644 pkg/code/chat/message_kin_purchases.go delete mode 100644 pkg/code/chat/message_merchant.go delete mode 100644 pkg/code/chat/message_tips.go delete mode 100644 pkg/code/chat/sender.go delete mode 100644 pkg/code/chat/sender_test.go delete mode 100644 pkg/code/chat/util.go delete mode 100644 pkg/code/chat/util_test.go create mode 100644 pkg/code/common/mint_test.go create mode 100644 pkg/code/config/config.go delete mode 100644 pkg/code/data/airdrop/memory/store.go delete mode 100644 pkg/code/data/airdrop/memory/store_test.go delete mode 100644 pkg/code/data/airdrop/postgres/model.go delete mode 100644 pkg/code/data/airdrop/postgres/store.go delete mode 100644 pkg/code/data/airdrop/postgres/store_test.go delete mode 100644 pkg/code/data/airdrop/store.go delete mode 100644 pkg/code/data/airdrop/tests/tests.go delete mode 100644 pkg/code/data/badgecount/badge_count.go delete mode 100644 pkg/code/data/badgecount/memory/store.go delete mode 100644 pkg/code/data/badgecount/memory/store_test.go delete mode 100644 pkg/code/data/badgecount/postgres/model.go delete mode 100644 pkg/code/data/badgecount/postgres/store.go delete mode 100644 pkg/code/data/badgecount/postgres/store_test.go delete mode 100644 pkg/code/data/badgecount/store.go delete mode 100644 pkg/code/data/badgecount/tests/tests.go delete mode 100644 pkg/code/data/chat/memory/store.go delete mode 100644 pkg/code/data/chat/memory/store_test.go delete mode 100644 pkg/code/data/chat/model.go delete mode 100644 pkg/code/data/chat/model_test.go delete mode 100644 pkg/code/data/chat/postgres/model.go delete mode 100644 pkg/code/data/chat/postgres/store.go delete mode 100644 pkg/code/data/chat/postgres/store_test.go delete mode 100644 pkg/code/data/chat/store.go delete mode 100644 pkg/code/data/chat/tests/tests.go delete mode 100644 pkg/code/data/commitment/commitment.go delete mode 100644 pkg/code/data/commitment/memory/store.go delete mode 100644 pkg/code/data/commitment/memory/store_test.go delete mode 100644 pkg/code/data/commitment/postgres/model.go delete mode 100644 pkg/code/data/commitment/postgres/store.go delete mode 100644 pkg/code/data/commitment/postgres/store_test.go delete mode 100644 pkg/code/data/commitment/store.go delete mode 100644 pkg/code/data/commitment/tests/tests.go delete mode 100644 pkg/code/data/contact/memory/store.go delete mode 100644 pkg/code/data/contact/memory/store_test.go delete mode 100644 pkg/code/data/contact/postgres/model.go delete mode 100644 pkg/code/data/contact/postgres/store.go delete mode 100644 pkg/code/data/contact/postgres/store_test.go delete mode 100644 pkg/code/data/contact/store.go delete mode 100644 pkg/code/data/contact/tests/tests.go delete mode 100644 pkg/code/data/event/memory/store.go delete mode 100644 pkg/code/data/event/memory/store_test.go delete mode 100644 pkg/code/data/event/postgres/model.go delete mode 100644 pkg/code/data/event/postgres/store.go delete mode 100644 pkg/code/data/event/postgres/store_test.go delete mode 100644 pkg/code/data/event/record.go delete mode 100644 pkg/code/data/event/store.go delete mode 100644 pkg/code/data/event/tests/tests.go delete mode 100644 pkg/code/data/invite/v2/memory/store.go delete mode 100644 pkg/code/data/invite/v2/memory/store_test.go delete mode 100644 pkg/code/data/invite/v2/postgres/model.go delete mode 100644 pkg/code/data/invite/v2/postgres/model_test.go delete mode 100644 pkg/code/data/invite/v2/postgres/store.go delete mode 100644 pkg/code/data/invite/v2/postgres/store_test.go delete mode 100644 pkg/code/data/invite/v2/store.go delete mode 100644 pkg/code/data/invite/v2/tests/tests.go delete mode 100644 pkg/code/data/login/login.go delete mode 100644 pkg/code/data/login/memory/store.go delete mode 100644 pkg/code/data/login/memory/store_test.go delete mode 100644 pkg/code/data/login/postgres/model.go delete mode 100644 pkg/code/data/login/postgres/store.go delete mode 100644 pkg/code/data/login/postgres/store_test.go delete mode 100644 pkg/code/data/login/store.go delete mode 100644 pkg/code/data/login/tests/tests.go delete mode 100644 pkg/code/data/payment/memory/store.go delete mode 100644 pkg/code/data/payment/memory/store_test.go delete mode 100644 pkg/code/data/payment/payment.go delete mode 100644 pkg/code/data/payment/postgres/model.go delete mode 100644 pkg/code/data/payment/postgres/store.go delete mode 100644 pkg/code/data/payment/postgres/store_test.go delete mode 100644 pkg/code/data/payment/store.go delete mode 100644 pkg/code/data/payment/tests/tests.go delete mode 100644 pkg/code/data/paywall/memory/store.go delete mode 100644 pkg/code/data/paywall/memory/store_test.go delete mode 100644 pkg/code/data/paywall/postgres/model.go delete mode 100644 pkg/code/data/paywall/postgres/store.go delete mode 100644 pkg/code/data/paywall/postgres/store_test.go delete mode 100644 pkg/code/data/paywall/record.go delete mode 100644 pkg/code/data/paywall/store.go delete mode 100644 pkg/code/data/paywall/tests/tests.go delete mode 100644 pkg/code/data/phone/memory/store.go delete mode 100644 pkg/code/data/phone/memory/stores_test.go delete mode 100644 pkg/code/data/phone/postgres/model.go delete mode 100644 pkg/code/data/phone/postgres/model_test.go delete mode 100644 pkg/code/data/phone/postgres/store.go delete mode 100644 pkg/code/data/phone/postgres/store_test.go delete mode 100644 pkg/code/data/phone/store.go delete mode 100644 pkg/code/data/phone/tests/tests.go delete mode 100644 pkg/code/data/preferences/memory/store.go delete mode 100644 pkg/code/data/preferences/memory/store_test.go delete mode 100644 pkg/code/data/preferences/postgres/model.go delete mode 100644 pkg/code/data/preferences/postgres/store.go delete mode 100644 pkg/code/data/preferences/postgres/store_test.go delete mode 100644 pkg/code/data/preferences/preferences.go delete mode 100644 pkg/code/data/preferences/store.go delete mode 100644 pkg/code/data/preferences/tests/tests.go delete mode 100644 pkg/code/data/push/memory/store.go delete mode 100644 pkg/code/data/push/memory/store_test.go delete mode 100644 pkg/code/data/push/postgres/model.go delete mode 100644 pkg/code/data/push/postgres/store.go delete mode 100644 pkg/code/data/push/postgres/store_test.go delete mode 100644 pkg/code/data/push/push_token.go delete mode 100644 pkg/code/data/push/store.go delete mode 100644 pkg/code/data/push/tests/tests.go delete mode 100644 pkg/code/data/treasury/memory/store.go delete mode 100644 pkg/code/data/treasury/memory/store_test.go delete mode 100644 pkg/code/data/treasury/postgres/model.go delete mode 100644 pkg/code/data/treasury/postgres/store.go delete mode 100644 pkg/code/data/treasury/postgres/store_test.go delete mode 100644 pkg/code/data/treasury/store.go delete mode 100644 pkg/code/data/treasury/tests/tests.go delete mode 100644 pkg/code/data/treasury/treasury.go delete mode 100644 pkg/code/data/treasury/treasury_test.go delete mode 100644 pkg/code/data/twitter/memory/store.go delete mode 100644 pkg/code/data/twitter/memory/store_test.go delete mode 100644 pkg/code/data/twitter/postgres/model.go delete mode 100644 pkg/code/data/twitter/postgres/store.go delete mode 100644 pkg/code/data/twitter/postgres/store_test.go delete mode 100644 pkg/code/data/twitter/store.go delete mode 100644 pkg/code/data/twitter/tests/tests.go delete mode 100644 pkg/code/data/twitter/user.go delete mode 100644 pkg/code/data/user/identity/memory/store.go delete mode 100644 pkg/code/data/user/identity/memory/store_test.go delete mode 100644 pkg/code/data/user/identity/postgres/model.go delete mode 100644 pkg/code/data/user/identity/postgres/model_test.go delete mode 100644 pkg/code/data/user/identity/postgres/store.go delete mode 100644 pkg/code/data/user/identity/postgres/store_test.go delete mode 100644 pkg/code/data/user/identity/store.go delete mode 100644 pkg/code/data/user/identity/tests/tests.go delete mode 100644 pkg/code/data/user/model.go delete mode 100644 pkg/code/data/user/model_test.go delete mode 100644 pkg/code/data/user/storage/memory/store.go delete mode 100644 pkg/code/data/user/storage/memory/store_test.go delete mode 100644 pkg/code/data/user/storage/postgres/model.go delete mode 100644 pkg/code/data/user/storage/postgres/model_test.go delete mode 100644 pkg/code/data/user/storage/postgres/store.go delete mode 100644 pkg/code/data/user/storage/postgres/store_test.go delete mode 100644 pkg/code/data/user/storage/store.go delete mode 100644 pkg/code/data/user/storage/tests/tests.go delete mode 100644 pkg/code/event/client.go delete mode 100644 pkg/code/localization/currency.go delete mode 100644 pkg/code/localization/device.go delete mode 100644 pkg/code/localization/keys.go delete mode 100644 pkg/code/localization/util.go delete mode 100644 pkg/code/push/badge_count.go delete mode 100644 pkg/code/push/data.go delete mode 100644 pkg/code/push/notifications.go delete mode 100644 pkg/code/push/text.go delete mode 100644 pkg/code/push/util.go rename pkg/code/server/{grpc => }/account/server.go (96%) rename pkg/code/server/{grpc => }/account/server_test.go (92%) rename pkg/code/server/{grpc => }/currency/currency.go (100%) delete mode 100644 pkg/code/server/grpc/badge/server.go delete mode 100644 pkg/code/server/grpc/badge/server_test.go delete mode 100644 pkg/code/server/grpc/chat/server.go delete mode 100644 pkg/code/server/grpc/chat/server_test.go delete mode 100644 pkg/code/server/grpc/contact/server.go delete mode 100644 pkg/code/server/grpc/contact/server_test.go delete mode 100644 pkg/code/server/grpc/device/server.go delete mode 100644 pkg/code/server/grpc/device/server_test.go delete mode 100644 pkg/code/server/grpc/invite/v2/server.go delete mode 100644 pkg/code/server/grpc/micropayment/server.go delete mode 100644 pkg/code/server/grpc/phone/server.go delete mode 100644 pkg/code/server/grpc/phone/server_test.go delete mode 100644 pkg/code/server/grpc/push/server.go delete mode 100644 pkg/code/server/grpc/push/server_test.go delete mode 100644 pkg/code/server/grpc/transaction/v2/action_handler.go delete mode 100644 pkg/code/server/grpc/transaction/v2/config.go delete mode 100644 pkg/code/server/grpc/transaction/v2/intent_handler.go delete mode 100644 pkg/code/server/grpc/transaction/v2/limits.go delete mode 100644 pkg/code/server/grpc/transaction/v2/proof.go delete mode 100644 pkg/code/server/grpc/transaction/v2/treasury.go delete mode 100644 pkg/code/server/grpc/user/config.go delete mode 100644 pkg/code/server/grpc/user/limiter.go delete mode 100644 pkg/code/server/grpc/user/server.go delete mode 100644 pkg/code/server/grpc/user/server_test.go rename pkg/code/server/{grpc => }/messaging/client.go (100%) rename pkg/code/server/{grpc => }/messaging/config.go (100%) rename pkg/code/server/{grpc => }/messaging/error.go (100%) rename pkg/code/server/{grpc => }/messaging/internal.go (100%) rename pkg/code/server/{grpc => }/messaging/message_handler.go (77%) rename pkg/code/server/{grpc => }/messaging/server.go (98%) rename pkg/code/server/{grpc => }/messaging/server_test.go (83%) rename pkg/code/server/{grpc => }/messaging/stream.go (100%) rename pkg/code/server/{grpc => }/messaging/testutil.go (89%) create mode 100644 pkg/code/server/micropayment/server.go rename pkg/code/server/{grpc => }/micropayment/server_test.go (56%) create mode 100644 pkg/code/server/transaction/action_handler.go rename pkg/code/server/{grpc/transaction/v2 => transaction}/airdrop.go (85%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/airdrop_test.go (100%) create mode 100644 pkg/code/server/transaction/config.go rename pkg/code/server/{grpc/transaction/v2 => transaction}/errors.go (87%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/intent.go (69%) create mode 100644 pkg/code/server/transaction/intent_handler.go rename pkg/code/server/{grpc/transaction/v2 => transaction}/intent_test.go (100%) create mode 100644 pkg/code/server/transaction/limits.go rename pkg/code/server/{grpc/transaction/v2 => transaction}/limits_test.go (100%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/local_simulation.go (86%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/local_simulation_test.go (100%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/metrics.go (100%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/onramp.go (82%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/onramp_test.go (100%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/server.go (60%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/swap.go (88%) rename pkg/code/server/{grpc/transaction/v2 => transaction}/testutil.go (100%) delete mode 100644 pkg/code/server/web/request/client.go delete mode 100644 pkg/code/server/web/request/model.go delete mode 100644 pkg/code/server/web/request/server.go delete mode 100644 pkg/code/server/web/request/util.go delete mode 100644 pkg/code/transaction/memo.go delete mode 100644 pkg/device/android/verifier.go delete mode 100644 pkg/device/composite/verifier.go delete mode 100644 pkg/device/ios/verifier.go delete mode 100644 pkg/device/memory/verifier.go delete mode 100644 pkg/device/verifier.go delete mode 100644 pkg/kikcode/currency.go delete mode 100644 pkg/kikcode/encoding/Array.h delete mode 100644 pkg/kikcode/encoding/Counted.h delete mode 100644 pkg/kikcode/encoding/Exception.cpp delete mode 100644 pkg/kikcode/encoding/Exception.h delete mode 100644 pkg/kikcode/encoding/GenericGF.cpp delete mode 100644 pkg/kikcode/encoding/GenericGF.h delete mode 100644 pkg/kikcode/encoding/GenericGFPoly.cpp delete mode 100644 pkg/kikcode/encoding/GenericGFPoly.h delete mode 100644 pkg/kikcode/encoding/IllegalArgumentException.cpp delete mode 100644 pkg/kikcode/encoding/IllegalArgumentException.h delete mode 100644 pkg/kikcode/encoding/IllegalStateException.h delete mode 100644 pkg/kikcode/encoding/ReaderException.h delete mode 100644 pkg/kikcode/encoding/ReedSolomonDecoder.cpp delete mode 100644 pkg/kikcode/encoding/ReedSolomonDecoder.h delete mode 100644 pkg/kikcode/encoding/ReedSolomonEncoder.cpp delete mode 100644 pkg/kikcode/encoding/ReedSolomonEncoder.h delete mode 100644 pkg/kikcode/encoding/ReedSolomonException.cpp delete mode 100644 pkg/kikcode/encoding/ReedSolomonException.h delete mode 100644 pkg/kikcode/encoding/ZXing.h delete mode 100644 pkg/kikcode/encoding/encoding.go delete mode 100644 pkg/kikcode/encoding/encoding_test.go delete mode 100644 pkg/kikcode/encoding/kikcode_constants.h delete mode 100644 pkg/kikcode/encoding/kikcode_encoding.cpp delete mode 100644 pkg/kikcode/encoding/kikcode_encoding.h delete mode 100644 pkg/kikcode/encoding/kikcode_wrapper.cpp delete mode 100644 pkg/kikcode/encoding/kikcode_wrapper.h delete mode 100644 pkg/kikcode/encoding/kikcodes.cpp delete mode 100644 pkg/kikcode/encoding/kikcodes.h delete mode 100644 pkg/kikcode/payload.go delete mode 100644 pkg/kikcode/qr.go delete mode 100644 pkg/kikcode/rendezvous.go delete mode 100644 pkg/kin/kin.go delete mode 100644 pkg/kin/memo.go delete mode 100644 pkg/kin/memo_test.go delete mode 100644 pkg/kin/utils.go delete mode 100644 pkg/kin/utils_test.go delete mode 100644 pkg/phone/mcc.go delete mode 100644 pkg/phone/memory/verifier.go delete mode 100644 pkg/phone/metadata.go delete mode 100644 pkg/phone/twilio/verifier.go delete mode 100644 pkg/phone/validation.go delete mode 100644 pkg/phone/verifier.go delete mode 100644 pkg/push/fcm/provider.go delete mode 100644 pkg/push/memory/provider.go delete mode 100644 pkg/push/provider.go delete mode 100644 pkg/twitter/client.go diff --git a/go.mod b/go.mod index ee78545e..bac76e90 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,12 @@ module github.com/code-payments/code-server go 1.23.0 require ( - firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886 + github.com/code-payments/code-protobuf-api v1.18.1-0.20250402182022-037f36525341 github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba - github.com/dghubble/oauth1 v0.7.3 github.com/emirpasic/gods v1.12.0 - github.com/envoyproxy/protoc-gen-validate v1.0.4 + github.com/envoyproxy/protoc-gen-validate v1.2.1 github.com/golang-jwt/jwt/v5 v5.0.0 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 @@ -23,41 +21,29 @@ require ( github.com/mr-tron/base58 v1.2.0 github.com/newrelic/go-agent/v3 v3.20.1 github.com/newrelic/go-agent/v3/integrations/nrpgx v1.0.0 - github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/ory/dockertest/v3 v3.7.0 - github.com/oschwald/maxminddb-golang v1.11.0 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pkg/errors v0.9.1 - github.com/rinchsan/device-check-go v1.3.0 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.8.1 github.com/spaolacci/murmur3 v1.1.0 github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.8.4 - github.com/twilio/twilio-go v0.26.0 github.com/vence722/base122-go v0.0.2 github.com/ybbus/jsonrpc v2.1.2+incompatible go.etcd.io/etcd/api/v3 v3.5.13 go.etcd.io/etcd/client/v3 v3.5.13 go.uber.org/zap v1.17.0 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.32.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/net v0.22.0 - golang.org/x/text v0.14.0 + golang.org/x/net v0.34.0 + golang.org/x/text v0.21.0 golang.org/x/time v0.5.0 - google.golang.org/api v0.170.0 - google.golang.org/grpc v1.63.2 - google.golang.org/protobuf v1.33.0 + google.golang.org/grpc v1.71.1 + google.golang.org/protobuf v1.36.6 ) require ( - cloud.google.com/go v0.112.0 // indirect - cloud.google.com/go/compute v1.24.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/firestore v1.14.0 // indirect - cloud.google.com/go/iam v1.1.6 // indirect - cloud.google.com/go/longrunning v0.5.5 // indirect - cloud.google.com/go/storage v1.36.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect @@ -71,18 +57,9 @@ require ( github.com/docker/docker v20.10.7+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/dvsekhvalnov/jose2go v1.5.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.4.7 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect @@ -94,9 +71,11 @@ require ( github.com/jackc/pgtype v1.8.1 // indirect github.com/jackc/pgx v3.6.2+incompatible // indirect github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect + github.com/kr/text v0.2.0 // indirect github.com/magiconair/properties v1.8.1 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/onsi/gomega v1.30.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect @@ -112,22 +91,12 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.18.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/appengine/v2 v2.0.1 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect + golang.org/x/sys v0.29.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/ini.v1 v1.51.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a167ee05..b3dad4be 100644 --- a/go.sum +++ b/go.sum @@ -17,46 +17,15 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= -cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= -cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= -cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= -cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= -cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= -cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= -cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= -cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -67,12 +36,7 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= -cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8= -cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -firebase.google.com/go/v4 v4.8.0 h1:ooJqjFEh1G6DQ5+wyb/RAXAgku0E2RzJeH6WauSpWSo= -firebase.google.com/go/v4 v4.8.0/go.mod h1:y+j6xX7BgBco/XaN+YExIBVm6pzvYutheDV3nprvbWc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -88,7 +52,6 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -106,7 +69,6 @@ github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -114,15 +76,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886 h1:PLbMVgpFwwhI6Ji+izq/NnJQGq41OKQ2dP3oPpUEcCA= -github.com/code-payments/code-protobuf-api v1.18.1-0.20240912180853-8e16dd113886/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.18.1-0.20250402182022-037f36525341 h1:kmB+HffbKF0BoJSTudH21IXTZy+PFPyukqWJIjppUkE= +github.com/code-payments/code-protobuf-api v1.18.1-0.20250402182022-037f36525341/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs= github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba h1:Bkp+gmeb6Y2PWXfkSCTMBGWkb2P1BujRDSjWeI+0j5I= github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba/go.mod h1:jSiifpiBpyBQ8q2R0MGEbkSgWC6sbdRTyDBntmW+j1E= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= @@ -143,8 +100,6 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE= -github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/cli v20.10.7+incompatible h1:pv/3NqibQKphWZiAskMzdz8w0PRbtTaEB+f6NwdU7Is= @@ -155,8 +110,6 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= -github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -164,15 +117,10 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= -github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -185,9 +133,8 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -201,7 +148,6 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -209,8 +155,6 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -218,9 +162,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -235,12 +176,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -251,20 +188,12 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -275,28 +204,14 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= -github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -306,7 +221,6 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 h1:FlFbCRLd5Jr4iYXZufAvgWN6A github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -449,8 +363,6 @@ github.com/newrelic/go-agent/v3 v3.20.1 h1:xxhPjE/j4z7n82FQV4izRjIkd4E10q4flqgzM github.com/newrelic/go-agent/v3 v3.20.1/go.mod h1:rT6ZUxJc5rQbWLyCtjqQCOcfb01lKRFbc1yMQkcboWM= github.com/newrelic/go-agent/v3/integrations/nrpgx v1.0.0 h1:5pj3uXyWB0fpgbeK1yW51go6Y57uRG8F7w5Nu6kIiCQ= github.com/newrelic/go-agent/v3/integrations/nrpgx v1.0.0/go.mod h1:G4vsr8xgPwFxxwJSbE982D7rswRFEfoCaXPQWWWQyQo= -github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= -github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -465,8 +377,6 @@ github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rm github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/dockertest/v3 v3.7.0 h1:Bijzonc69Ont3OU0a3TWKJ1Rzlh3TsDXP1JrTAkSmsM= github.com/ory/dockertest/v3 v3.7.0/go.mod h1:PvCCgnP7AfBZeVrzwiUTjZx/IUXlGLC1zQlUQrLIlUE= -github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0= -github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= @@ -490,12 +400,9 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rinchsan/device-check-go v1.3.0 h1:Rk7L/sJR3hpJ2bBvXhhu42PvmeA5+rmy8uVihO9X8A4= -github.com/rinchsan/device-check-go v1.3.0/go.mod h1:xDdGHphsyiTYLfq36DlAn8M8ir2iyUS5nOMj62sF3hU= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= @@ -533,24 +440,16 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/twilio/twilio-go v0.26.0 h1:wFW4oTe3/LKt6bvByP7eio8JsjtaLHjMQKOUEzQry7U= -github.com/twilio/twilio-go v0.26.0/go.mod h1:lz62Hopu4vicpQ056H5TJ0JE4AP0rS3sQ35/ejmgOwE= github.com/vence722/base122-go v0.0.2 h1:U9hyrbOsw2c1jdg2QPNPvt15cdszVdn7uaxU8K2VrYg= github.com/vence722/base122-go v0.0.2/go.mod h1:3bzDQsujP7wNZSqg6jsnNXF+XABfiET4y5qM+YtFPzs= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -566,8 +465,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4= @@ -582,22 +479,18 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -629,10 +522,9 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -659,7 +551,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -670,8 +561,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -706,20 +595,12 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -729,17 +610,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -750,10 +620,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -798,50 +664,24 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -905,20 +745,12 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -938,27 +770,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM= -google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7C80= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.73.0/go.mod h1:lbd/q6BRFJbdpV6OUCXstVeiI5mL/d3/WifG7iNKnjI= -google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48= -google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -967,10 +778,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/appengine/v2 v2.0.1 h1:jTGfiRmR5qoInpT3CXJ72GJEB4owDGEKN+xRDA6ekBY= -google.golang.org/appengine/v2 v2.0.1/go.mod h1:XgltgQxPOF3ShivrVrZyfvYCx8Dunh73bKjUuXUZb8Q= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -995,7 +802,6 @@ google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -1008,51 +814,11 @@ google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1066,24 +832,11 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1094,15 +847,11 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1113,7 +862,6 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/cluster/grpc/example/cluster.go b/pkg/cluster/grpc/example/cluster.go index aa3459fb..e0407669 100644 --- a/pkg/cluster/grpc/example/cluster.go +++ b/pkg/cluster/grpc/example/cluster.go @@ -1,5 +1,8 @@ package example +// Example uses deprecated service + +/* import ( "context" "sync" @@ -185,3 +188,4 @@ func (r Result) Mismatch() bool { func (r Result) Cached() bool { return (r>>16)&0x02 == 2 } +*/ diff --git a/pkg/cluster/grpc/example/cluster_test.go b/pkg/cluster/grpc/example/cluster_test.go index 3b32febe..3d119115 100644 --- a/pkg/cluster/grpc/example/cluster_test.go +++ b/pkg/cluster/grpc/example/cluster_test.go @@ -1,5 +1,6 @@ package example +/* import ( "context" "fmt" @@ -278,3 +279,4 @@ func TestCacheInvalidation(t *testing.T) { return cached }, 5*time.Second, 10*time.Millisecond) } +*/ diff --git a/pkg/code/antispam/address.go b/pkg/code/antispam/address.go deleted file mode 100644 index cafcafa7..00000000 --- a/pkg/code/antispam/address.go +++ /dev/null @@ -1,11 +0,0 @@ -package antispam - -import ( - "github.com/code-payments/code-server/pkg/code/common" -) - -// todo: Put in a DB somehwere? Or make configurable? -// todo: Needs tests -func isExternalAddressBanned(account *common.Account) bool { - return false -} diff --git a/pkg/code/antispam/airdrop.go b/pkg/code/antispam/airdrop.go index 18223328..82fa48a7 100644 --- a/pkg/code/antispam/airdrop.go +++ b/pkg/code/antispam/airdrop.go @@ -2,132 +2,23 @@ package antispam import ( "context" - "time" - - "github.com/sirupsen/logrus" "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/user/identity" "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/grpc/client" "github.com/code-payments/code-server/pkg/metrics" ) -var ( - deviceCheckV2ReleaseDate = time.Date(2023, time.October, 26, 0, 0, 0, 0, time.UTC) -) - // AllowWelcomeBonus determines whether a phone-verified owner account can receive -// the welcome bonus. The objective here is to limit attacks against our airdropper's -// Kin balance. -// -// todo: This needs tests +// the welcome bonus. func (g *Guard) AllowWelcomeBonus(ctx context.Context, owner *common.Account) (bool, error) { tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowWelcomeBonus") defer tracer.End() - log := g.log.WithFields(logrus.Fields{ - "method": "AllowWelcomeBonus", - "owner": owner.PublicKey().ToBase58(), - }) - log = client.InjectLoggingMetadata(ctx, log) - - // Deny abusers from known IPs - if isIpBanned(ctx) { - log.Info("ip is banned") - recordDenialEvent(ctx, actionWelcomeBonus, "ip banned") - return false, nil - } - - verification, err := g.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - // Owner account was never phone verified, so deny the action. - log.Info("owner account is not phone verified") - recordDenialEvent(ctx, actionWelcomeBonus, "not phone verified") - return false, nil - } else if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting phone verification record") - return false, err - } - - log = log.WithField("phone", verification.PhoneNumber) - - if g.isSuspiciousWelcomeBonus(ctx, verification.PhoneNumber) { - log.Info("denying suspicious welcome bonus") - recordDenialEvent(ctx, actionWelcomeBonus, "suspicious welcome bonus") - return false, nil - } - - // Deny abusers from known phone ranges - if isSanctionedPhoneNumber(verification.PhoneNumber) { - log.Info("denying phone prefix") - recordDenialEvent(ctx, actionWelcomeBonus, "phone prefix banned") - return false, nil - } - - user, err := g.data.GetUserByPhoneView(ctx, verification.PhoneNumber) - switch err { - case nil: - // Deny banned users forever - if user.IsBanned { - log.Info("denying banned user") - recordDenialEvent(ctx, actionWelcomeBonus, "user banned") - return false, nil - } - - // Staff users have unlimited access to enable testing and demoing. - if user.IsStaffUser { - return true, nil - } - case identity.ErrNotFound: - default: - tracer.OnError(err) - log.WithError(err).Warn("failure getting user identity by phone view") - return false, err - } - - phoneEvent, err := g.data.GetLatestPhoneEventForNumberByType(ctx, verification.PhoneNumber, phone.EventTypeVerificationCodeSent) - switch err { - case nil: - // Deny from regions where we're currently under attack - if phoneEvent.PhoneMetadata.MobileCountryCode != nil { - if _, ok := g.conf.restrictedMobileCountryCodes[*phoneEvent.PhoneMetadata.MobileCountryCode]; ok { - log.WithField("region", *phoneEvent.PhoneMetadata.MobileCountryCode).Info("region is restricted") - recordDenialEvent(ctx, actionWelcomeBonus, "region restricted") - return false, nil - } - } - - // Deny from mobile networks where we're currently under attack - if phoneEvent.PhoneMetadata.MobileNetworkCode != nil { - if _, ok := g.conf.restrictedMobileNetworkCodes[*phoneEvent.PhoneMetadata.MobileNetworkCode]; ok { - log.WithField("mobile_network", *phoneEvent.PhoneMetadata.MobileNetworkCode).Info("mobile network is restricted") - recordDenialEvent(ctx, actionWelcomeBonus, "mobile network restricted") - return false, nil - } - } - case phone.ErrEventNotFound: - default: - log.WithError(err).Warn("failure getting phone event") - return false, err - } - return true, nil } -// Special rules based on observed behaviour -func (g *Guard) isSuspiciousWelcomeBonus(ctx context.Context, phoneNumber string) bool { - return false -} - // AllowReferralBonus determines whether a phone-verified owner account can receive -// a referral bonus. The objective here is to limit attacks against our airdropper's -// Kin balance. -// -// todo: This needs tests +// a referral bonus. func (g *Guard) AllowReferralBonus( ctx context.Context, referrerOwner, @@ -140,127 +31,5 @@ func (g *Guard) AllowReferralBonus( tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowReferralBonus") defer tracer.End() - log := g.log.WithFields(logrus.Fields{ - "method": "AllowReferralBonus", - "referrer_owner": referrerOwner.PublicKey().ToBase58(), - "onboarded_owner": onboardedOwner.PublicKey().ToBase58(), - "exchanged_in": exchangedIn, - "native_amount": nativeAmount, - }) - log = client.InjectLoggingMetadata(ctx, log) - - if quarksGivenByReferrer < g.conf.minReferralAmount { - log.Info("insufficient quarks given by referrer") - recordDenialEvent(ctx, actionReferralBonus, "insufficient quarks given by referrer") - return false, nil - } - - for _, owner := range []*common.Account{referrerOwner, onboardedOwner} { - verification, err := g.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - // Owner account was never phone verified, so deny the action. - log.Info("owner account is not phone verified") - recordDenialEvent(ctx, actionReferralBonus, "not phone verified") - return false, nil - } else if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting phone verification record") - return false, err - } - - log := log.WithField("phone", verification.PhoneNumber) - - // Deny users from sanctioned countries - if isSanctionedPhoneNumber(verification.PhoneNumber) { - log.Info("denying sanctioned country") - recordDenialEvent(ctx, actionReferralBonus, "sanctioned country") - return false, nil - } - - if owner.PublicKey().ToBase58() == referrerOwner.PublicKey().ToBase58() && isSusipiciousReferral(verification.PhoneNumber, exchangedIn, nativeAmount) { - log.Info("denying suspicious referral") - recordDenialEvent(ctx, actionReferralBonus, "suspicious referral denied") - return false, nil - } - - user, err := g.data.GetUserByPhoneView(ctx, verification.PhoneNumber) - switch err { - case nil: - // Deny banned users forever - if user.IsBanned { - log.Info("denying banned user") - recordDenialEvent(ctx, actionReferralBonus, "user banned") - return false, nil - } - - // Staff users have unlimited access to enable testing and demoing. - if user.IsStaffUser { - return true, nil - } - - if owner.PublicKey().ToBase58() == onboardedOwner.PublicKey().ToBase58() && user.CreatedAt.Before(deviceCheckV2ReleaseDate) { - log.Info("denying pre-device check v2 onboarded user") - recordDenialEvent(ctx, actionReferralBonus, "pre-device check v2 onboarded user") - return false, nil - } - case identity.ErrNotFound: - default: - tracer.OnError(err) - log.WithError(err).Warn("failure getting user identity by phone view") - return false, err - } - - phoneEvent, err := g.data.GetLatestPhoneEventForNumberByType(ctx, verification.PhoneNumber, phone.EventTypeVerificationCodeSent) - switch err { - case nil: - // Deny from regions where we're currently under attack - if phoneEvent.PhoneMetadata.MobileCountryCode != nil { - if _, ok := g.conf.restrictedMobileCountryCodes[*phoneEvent.PhoneMetadata.MobileCountryCode]; ok { - log.WithField("region", *phoneEvent.PhoneMetadata.MobileCountryCode).Info("region is restricted") - recordDenialEvent(ctx, actionReferralBonus, "region restricted") - return false, nil - } - - } - - // Deny from mobile networks where we're currently under attack - if phoneEvent.PhoneMetadata.MobileNetworkCode != nil { - if _, ok := g.conf.restrictedMobileNetworkCodes[*phoneEvent.PhoneMetadata.MobileNetworkCode]; ok { - log.WithField("mobile_network", *phoneEvent.PhoneMetadata.MobileNetworkCode).Info("mobile network is restricted") - recordDenialEvent(ctx, actionReferralBonus, "mobile network restricted") - return false, nil - } - } - case phone.ErrEventNotFound: - default: - log.WithError(err).Warn("failure getting phone event") - return false, err - } - } - - count, err := g.data.GetIntentCountWithOwnerInteractionsForAntispam( - ctx, - airdropperOwner.PublicKey().ToBase58(), - referrerOwner.PublicKey().ToBase58(), - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Now().Add(-24*time.Hour), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, err - } - - if count >= g.conf.maxReferralsPerDay { - log.Info("phone is rate limited by daily referral bonus count") - recordDenialEvent(ctx, actionReferralBonus, "daily limit exceeded") - return false, nil - } - return true, nil } - -// Special rules based on observed behaviour -func isSusipiciousReferral(phoneNumber string, exchangedIn currency.Code, nativeAmount float64) bool { - return false -} diff --git a/pkg/code/antispam/config.go b/pkg/code/antispam/config.go deleted file mode 100644 index 5c522f5f..00000000 --- a/pkg/code/antispam/config.go +++ /dev/null @@ -1,216 +0,0 @@ -package antispam - -import ( - "time" - - "github.com/code-payments/code-server/pkg/kin" -) - -// todo: migrate this to the new way of doing configs - -const ( - // todo: separate limits by public/private? - defaultPaymentsPerDay = 250 - defaultPaymentsPerHour = 50 - defaultPaymentsPerFiveMinutes = 25 - defaultTimePerPayment = time.Second / 4 - - defaultPhoneVerificationInterval = 10 * time.Minute - defaultPhoneVerificationsPerInterval = 5 - defaultTimePerSmsVerificationCodeSend = 30 * time.Second - defaultTimePerSmsVerificationCodeCheck = time.Second - - defaultMaxNewRelationshipsPerDay = 50 - - defaultMinReferralAmount = 100 * kin.QuarksPerKin - defaultMaxReferralsPerDay = 10 -) - -type conf struct { - paymentsPerDay uint64 - paymentsPerHour uint64 - paymentsPerFiveMinutes uint64 - timePerPayment time.Duration - - phoneVerificationInterval time.Duration - phoneVerificationsPerInternval uint64 - timePerSmsVerificationCodeSend time.Duration - timePerSmsVerificationCodeCheck time.Duration - - maxNewRelationshipsPerDay uint64 - - minReferralAmount uint64 - maxReferralsPerDay uint64 - - restrictedMobileCountryCodes map[int]struct{} - restrictedMobileNetworkCodes map[int]struct{} - - androidDevsByPhoneNumber map[string]struct{} -} - -// Option configures a Guard with an overrided configuration value -type Option func(c *conf) - -// WithDailyPaymentLimit overrides the default daily payment limit. The value -// specifies the maximum number of payments that can initiated per phone number -// per day. -func WithDailyPaymentLimit(limit uint64) Option { - return func(c *conf) { - c.paymentsPerDay = limit - } -} - -// WithHourlyPaymentLimit overrides the default hourly payment limit. The value -// specifies the maximum number of payments that can initiated per phone number -// per hour. -func WithHourlyPaymentLimit(limit uint64) Option { - return func(c *conf) { - c.paymentsPerHour = limit - } -} - -// WithFiveMinutePaymentLimit overrides the default five minute payment limit. -// The value specifies the maximum number of payments that can initiated per -// phone number per five minutes. -func WithFiveMinutePaymentLimit(limit uint64) Option { - return func(c *conf) { - c.paymentsPerFiveMinutes = limit - } -} - -// WithPaymentRateLimit overrides the default payment rate limit. The value -// specifies the minimum time between payments per phone number. -func WithPaymentRateLimit(d time.Duration) Option { - return func(c *conf) { - c.timePerPayment = d - } -} - -// WithPhoneVerificationInterval overrides the default phone verification interval. -// The value specifies the time window at which unique verifications are evaluated -// per phone number. -func WithPhoneVerificationInterval(d time.Duration) Option { - return func(c *conf) { - c.phoneVerificationInterval = d - } -} - -// WithPhoneVerificationsPerInterval overrides the default number of phone verifications -// in an interval. The value specifies the number of unique phone verifications that can -// happen within the configurable time window per phone number. -func WithPhoneVerificationsPerInterval(limit uint64) Option { - return func(c *conf) { - c.phoneVerificationsPerInternval = limit - } -} - -// WithTimePerSmsVerificationCodeSend overrides the default time per SMS verifications codes -// sent. The value specifies the minimum time that must be waited to send consecutive SMS -// verification codes per phone number. -func WithTimePerSmsVerificationCodeSend(d time.Duration) Option { - return func(c *conf) { - c.timePerSmsVerificationCodeSend = d - } -} - -// WithTimePerSmsVerificationCheck overrides the default time per SMS verifications codes -// checked. The value specifies the minimum time that must be waited to consecutively check -// SMS verification codes per phone number. -func WithTimePerSmsVerificationCheck(d time.Duration) Option { - return func(c *conf) { - c.timePerSmsVerificationCodeCheck = d - } -} - -// WithMaxNewRelationshipsPerDay overrides the default maximum number of new relationships -// a phone number can create per day. -func WithMaxNewRelationshipsPerDay(limit uint64) Option { - return func(c *conf) { - c.maxNewRelationshipsPerDay = limit - } -} - -// WithMinReferralAmount overrides the default minimum referral amount. The value specifies -// the minimum amount that must be given to a new user to consider a referral bonus. -func WithMinReferralAmount(amount uint64) Option { - return func(c *conf) { - c.minReferralAmount = amount - } -} - -// WithMaxReferralsPerDay overrides the default maximum referrals per day. The value specifies -// the maximum number of times a user can be given a referral bonus in a day. -func WithMaxReferralsPerDay(limit uint64) Option { - return func(c *conf) { - c.maxReferralsPerDay = limit - } -} - -// WithRestrictedMobileCountryCodes overrides the default set of restricted mobile country -// codes. The values specify the mobile country codes with restricted access to prevent -// spam waves from problematic regions. -func WithRestrictedMobileCountryCodes(mccs ...int) Option { - return func(c *conf) { - c.restrictedMobileCountryCodes = make(map[int]struct{}) - for _, mcc := range mccs { - c.restrictedMobileCountryCodes[mcc] = struct{}{} - } - } -} - -// WithAndroidDevs configures a set of open source Android devs that get to bypass certain -// antispam measures to enable testing. Android is currently behind the latest antispam -// system requirements, and will fail things like device attestation. -func WithAndroidDevs(phoneNumbers ...string) Option { - return func(c *conf) { - c.androidDevsByPhoneNumber = make(map[string]struct{}) - for _, phoneNumber := range phoneNumbers { - c.androidDevsByPhoneNumber[phoneNumber] = struct{}{} - } - } -} - -// WithRestrictedMobileNetworkCodes overrides the default set of restricted mobile network -// codes. The values specify the mobile network codes with restricted access to prevent -// attacks from fraudulent operators. -// -// todo: Need to be careful with these. MNC may not be unique to a MCC. The -// ones provided here don't exist anywhere. -func WithRestrictedMobileNetworkCodes(mncs ...int) Option { - return func(c *conf) { - c.restrictedMobileNetworkCodes = make(map[int]struct{}) - for _, mnc := range mncs { - c.restrictedMobileNetworkCodes[mnc] = struct{}{} - } - } -} - -func applyOptions(opts ...Option) *conf { - defaultConfig := &conf{ - paymentsPerDay: defaultPaymentsPerDay, - paymentsPerHour: defaultPaymentsPerHour, - paymentsPerFiveMinutes: defaultPaymentsPerFiveMinutes, - timePerPayment: defaultTimePerPayment, - - phoneVerificationInterval: defaultPhoneVerificationInterval, - phoneVerificationsPerInternval: defaultPhoneVerificationsPerInterval, - timePerSmsVerificationCodeSend: defaultTimePerSmsVerificationCodeSend, - timePerSmsVerificationCodeCheck: defaultTimePerSmsVerificationCodeCheck, - - maxNewRelationshipsPerDay: defaultMaxNewRelationshipsPerDay, - - minReferralAmount: defaultMinReferralAmount, - maxReferralsPerDay: defaultMaxReferralsPerDay, - - restrictedMobileCountryCodes: make(map[int]struct{}), - restrictedMobileNetworkCodes: make(map[int]struct{}), - - androidDevsByPhoneNumber: make(map[string]struct{}), - } - - for _, opt := range opts { - opt(defaultConfig) - } - - return defaultConfig -} diff --git a/pkg/code/antispam/guard.go b/pkg/code/antispam/guard.go index 755b18ed..d98ad3b7 100644 --- a/pkg/code/antispam/guard.go +++ b/pkg/code/antispam/guard.go @@ -1,54 +1,26 @@ package antispam import ( - "github.com/oschwald/maxminddb-golang" "github.com/sirupsen/logrus" - xrate "golang.org/x/time/rate" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/device" - "github.com/code-payments/code-server/pkg/rate" ) -// todo: Generally, this package has evolved quickly, which means testing and -// code structure isn't the most ideal. A refactor is needed at some point. -// For example, there's a lot of shared commonalities between the various -// Allow methods that are copied and not tested (phone prefix, ip, banned -// users, etc.). - // Guard is an antispam guard that checks whether operations of interest are // allowed to be performed. // // Note: Implementation assumes distributed locking has already occurred for // all methods. type Guard struct { - log *logrus.Entry - data code_data.Provider - deviceVerifier device.Verifier - maxmind *maxminddb.Reader - limiter *limiter - conf *conf + log *logrus.Entry + data code_data.Provider } func NewGuard( data code_data.Provider, - deviceVerifier device.Verifier, - maxmind *maxminddb.Reader, - opts ...Option, ) *Guard { - conf := applyOptions(opts...) - - // todo: need a global rate limiter - limiter := newLimiter(func(r float64) rate.Limiter { - return rate.NewLocalRateLimiter(xrate.Limit(r)) - }, float64(xrate.Every(conf.timePerPayment))) - return &Guard{ - log: logrus.StandardLogger().WithField("type", "antispam/guard"), - data: data, - deviceVerifier: deviceVerifier, - maxmind: maxmind, - limiter: limiter, - conf: conf, + log: logrus.StandardLogger().WithField("type", "antispam/guard"), + data: data, } } diff --git a/pkg/code/antispam/guard_test.go b/pkg/code/antispam/guard_test.go deleted file mode 100644 index ed6a5257..00000000 --- a/pkg/code/antispam/guard_test.go +++ /dev/null @@ -1,945 +0,0 @@ -package antispam - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/currency" - memory_device_verifier "github.com/code-payments/code-server/pkg/device/memory" - phone_lib "github.com/code-payments/code-server/pkg/phone" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/testutil" -) - -type testEnv struct { - ctx context.Context - guard *Guard - data code_data.Provider -} - -func setup(t *testing.T) (env testEnv) { - env.ctx = context.Background() - env.data = code_data.NewTestDataProvider() - env.guard = NewGuard( - env.data, - memory_device_verifier.NewMemoryDeviceVerifier(), - nil, - - // Intent limits - WithDailyPaymentLimit(5), - WithPaymentRateLimit(time.Second), - WithMaxNewRelationshipsPerDay(5), - - // Phone verification limits - WithPhoneVerificationsPerInterval(3), - WithTimePerSmsVerificationCodeSend(time.Second), - WithTimePerSmsVerificationCheck(time.Second), - ) - - return env -} - -func TestAllowSendPayment_NotPhoneVerified(t *testing.T) { - env := setup(t) - - // Account isn't phone verified, so it cannot be used for payments - for _, isPublic := range []bool{true, false} { - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowSendPayment(env.ctx, testutil.NewRandomAccount(t), isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.False(t, allow) - } - } -} - -func TestAllowSendPayment_TimeBetweenIntents(t *testing.T) { - for _, isPublic := range []bool{true, false} { - env := setup(t) - - phoneNumber := "+12223334444" - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - } - - // First payment should always succeed - allow, err := env.guard.AllowSendPayment(env.ctx, ownerAccount1, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.True(t, allow) - - // Subsequent payments should fail hitting the time-based rate limit - // regardless of owner account associated with the phone number - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - allow, err = env.guard.AllowSendPayment(env.ctx, ownerAccount, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.False(t, allow) - } - - // After waiting the timeout, the payments should be allowed regardless of - // owner account associated with the phone number - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - // todo: need a better way to test with time than waiting - time.Sleep(time.Second) - - allow, err = env.guard.AllowSendPayment(env.ctx, ownerAccount, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.True(t, allow) - } - } -} - -func TestAllowSendPayment_TimeBasedLimit(t *testing.T) { - for _, isPublic := range []bool{true, false} { - env := setup(t) - - phoneNumber := "+12223334444" - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - } - - // First payment should always succeed - allow, err := env.guard.AllowSendPayment(env.ctx, ownerAccount1, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.True(t, allow) - - // Consume the daily limit of payments - for _, state := range []intent.State{ - intent.StateUnknown, - intent.StatePending, - intent.StateFailed, - intent.StateConfirmed, - } { - for i := 0; i < 2; i++ { - simulateSentPayment(t, env, ownerAccount1, isPublic, state) - } - } - - // Payments are denied after breaching the daily count limit regardless of - // owner account associated with the phone number - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - // todo: need a better way to test with time than waiting - time.Sleep(time.Second) - - allow, err := env.guard.AllowSendPayment(env.ctx, ownerAccount, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.False(t, allow) - } - } -} - -func TestAllowSendPayment_PublicAndPrivateSeparated(t *testing.T) { - for _, isPublic := range []bool{true, false} { - env := setup(t) - - phoneNumber := "+12223334444" - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - } - - // First payment should always succeed - allow, err := env.guard.AllowSendPayment(env.ctx, ownerAccount1, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.True(t, allow) - - // Consume the daily limit of payments of the other type - for _, state := range []intent.State{ - intent.StateUnknown, - intent.StatePending, - intent.StateFailed, - intent.StateConfirmed, - } { - for i := 0; i < 2; i++ { - simulateSentPayment(t, env, ownerAccount1, !isPublic, state) - } - } - - // Payments are still allowed after breaching the daily count limit of - // the other kind - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - // todo: need a better way to test with time than waiting - time.Sleep(time.Second) - - allow, err := env.guard.AllowSendPayment(env.ctx, ownerAccount, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.True(t, allow) - } - } -} - -func TestAllowSendPayment_StaffUser(t *testing.T) { - for _, isPublic := range []bool{true, false} { - env := setup(t) - - for i, isStaffUser := range []bool{true, false} { - phoneNumber := fmt.Sprintf("+1800555000%d", i) - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - require.NoError(t, env.data.PutUser(env.ctx, &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - IsStaffUser: isStaffUser, - CreatedAt: time.Now(), - })) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - verification := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - require.NoError(t, env.guard.data.SavePhoneVerification(env.ctx, verification)) - } - - // First payment should always be successful, regardless of user status - allow, err := env.guard.AllowSendPayment(env.ctx, ownerAccount1, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.True(t, allow) - - // Consume the remaining daily limits for payments. - for i := 0; i < 10; i++ { - simulateSentPayment(t, env, ownerAccount2, isPublic, intent.StateConfirmed) - } - - // Staff users should not be subject to any denials - allow, err = env.guard.AllowSendPayment(env.ctx, ownerAccount2, isPublic, testutil.NewRandomAccount(t)) - require.NoError(t, err) - assert.Equal(t, isStaffUser, allow) - } - } -} - -func TestAllowReceivePayments_NotPhoneVerified(t *testing.T) { - for _, isPublic := range []bool{true, false} { - env := setup(t) - - // Account isn't phone verified, so it cannot be used for payments - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowReceivePayments(env.ctx, testutil.NewRandomAccount(t), isPublic) - require.NoError(t, err) - assert.False(t, allow) - } - } -} - -func TestAllowReceivePayments_TimeBetweenIntents(t *testing.T) { - for _, isPublic := range []bool{true, false} { - env := setup(t) - - phoneNumber := "+12223334444" - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - } - - // First payment should always succeed - allow, err := env.guard.AllowReceivePayments(env.ctx, ownerAccount1, isPublic) - require.NoError(t, err) - assert.True(t, allow) - - // Subsequent payments should fail hitting the time-based rate limit - // regardless of owner account associated with the phone number - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - allow, err = env.guard.AllowReceivePayments(env.ctx, ownerAccount, isPublic) - require.NoError(t, err) - assert.False(t, allow) - } - - // After waiting the timeout, the payments should be allowed regardless of - // owner account associated with the phone number - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - // todo: need a better way to test with time than waiting - time.Sleep(time.Second) - - allow, err = env.guard.AllowReceivePayments(env.ctx, ownerAccount, isPublic) - require.NoError(t, err) - assert.True(t, allow) - } - } -} - -func TestAllowReceivePayments_TimeBasedLimit(t *testing.T) { - for _, isPublic := range []bool{true, false} { - env := setup(t) - - phoneNumber := "+12223334444" - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - } - - // First payment should always succeed - allow, err := env.guard.AllowReceivePayments(env.ctx, ownerAccount1, isPublic) - require.NoError(t, err) - assert.True(t, allow) - - // Consume the daily limit of payments - for _, state := range []intent.State{ - intent.StateUnknown, - intent.StatePending, - intent.StateFailed, - intent.StateConfirmed, - } { - for i := 0; i < 2; i++ { - simulateReceivedPayment(t, env, ownerAccount1, isPublic, state) - } - } - - // Payments are denied after breaching the daily count limit regardless of - // owner account associated with the phone number - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - // todo: need a better way to test with time than waiting - time.Sleep(time.Second) - - allow, err := env.guard.AllowReceivePayments(env.ctx, ownerAccount, isPublic) - require.NoError(t, err) - assert.False(t, allow) - } - } -} - -func TestAllowReceivePayments_StaffUser(t *testing.T) { - for _, isPublic := range []bool{true, false} { - env := setup(t) - - for i, isStaffUser := range []bool{true, false} { - phoneNumber := fmt.Sprintf("+1800555000%d", i) - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - require.NoError(t, env.data.PutUser(env.ctx, &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - IsStaffUser: isStaffUser, - CreatedAt: time.Now(), - })) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - - verification := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - require.NoError(t, env.guard.data.SavePhoneVerification(env.ctx, verification)) - } - - // First payment should always be successful, regardless of user status - allow, err := env.guard.AllowReceivePayments(env.ctx, ownerAccount1, isPublic) - require.NoError(t, err) - assert.True(t, allow) - - // Consume the remaining daily limits for payments. - for i := 0; i < 10; i++ { - simulateReceivedPayment(t, env, ownerAccount2, isPublic, intent.StateConfirmed) - } - - // Staff users should not be subject to any denials - allow, err = env.guard.AllowReceivePayments(env.ctx, ownerAccount2, isPublic) - require.NoError(t, err) - assert.Equal(t, isStaffUser, allow) - } - } -} - -func TestAllowOpenAccounts_HappyPath(t *testing.T) { - for _, testCase := range []intent.State{intent.StateUnknown, intent.StatePending, intent.StateConfirmed} { - env := setup(t) - - phoneNumber := "+18005550000" - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - // Account isn't phone verified, so it cannot be created - for i := 0; i < 5; i++ { - allow, _, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.False(t, allow) - } - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - verification := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - require.NoError(t, env.guard.data.SavePhoneVerification(env.ctx, verification)) - } - - // New accounts are always denied when using a fake or unverifiable device. - for i := 0; i < 5; i++ { - allow, reason, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.InvalidDeviceToken)) - require.NoError(t, err) - assert.False(t, allow) - assert.Equal(t, ReasonUnsupportedDevice, reason) - - allow, reason, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, nil) - require.NoError(t, err) - assert.False(t, allow) - assert.Equal(t, ReasonUnsupportedDevice, reason) - } - - // The first account creation should always be successful - allow, _, successCallback, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.True(t, allow) - require.NotNil(t, successCallback) - - // Have a set of account creations that are in an unsuccessful terminal - // state that won't be fixed - for _, state := range []intent.State{intent.StateRevoked} { - simulateAccountCreation(t, env, ownerAccount1, state, time.Now()) - } - - // Account creation is unaffected by previous creations that didn't - // result in success - allow, _, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.True(t, allow) - - // Consume the remaining lifetime limit of account creations - simulateAccountCreation(t, env, ownerAccount1, testCase, time.Now().Add(-10*365*24*time.Hour)) - - // Account creations are denied after breaching the daily count limit regardless - // of owner account associated with the phone number - for i := 0; i < 5; i++ { - allow, reason, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.False(t, allow) - assert.Equal(t, ReasonTooManyFreeAccountsForPhoneNumber, reason) - } - - // New accounts are always denied within the same device - require.NoError(t, successCallback()) - - newPhoneNumber := "+11234567890" - verification := &phone.Verification{ - PhoneNumber: newPhoneNumber, - OwnerAccount: ownerAccount2.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - require.NoError(t, env.guard.data.SavePhoneVerification(env.ctx, verification)) - - for i := 0; i < 5; i++ { - allow, reason, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.False(t, allow) - assert.Equal(t, ReasonTooManyFreeAccountsForDevice, reason) - } - } -} - -func TestAllowOpenAccounts_StaffUser(t *testing.T) { - env := setup(t) - - for i, isStaffUser := range []bool{true, false} { - phoneNumber := fmt.Sprintf("+1800555000%d", i) - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - require.NoError(t, env.data.PutUser(env.ctx, &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - IsStaffUser: isStaffUser, - CreatedAt: time.Now(), - })) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - - verification := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - require.NoError(t, env.guard.data.SavePhoneVerification(env.ctx, verification)) - } - - // First account creation is always successful, regardless of user status - allow, _, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.True(t, allow) - - // Consume the remaining daily limit of account creations - simulateAccountCreation(t, env, ownerAccount1, intent.StateConfirmed, time.Now()) - - // Staff users should not be subject to any denials - allow, _, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.Equal(t, isStaffUser, allow) - } -} - -func TestAllowEstablishNewRelationship_HappyPath(t *testing.T) { - env := setup(t) - - for i, testCase := range []intent.State{intent.StateUnknown, intent.StatePending, intent.StateConfirmed} { - phoneNumber := fmt.Sprintf("+1800555000%d", i) - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - // Account isn't phone verified, so it cannot be created - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowEstablishNewRelationship(env.ctx, ownerAccount1, "getcode.com") - require.NoError(t, err) - assert.False(t, allow) - } - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - verification := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - require.NoError(t, env.guard.data.SavePhoneVerification(env.ctx, verification)) - } - - // Daily limit not consumed - allow, err := env.guard.AllowEstablishNewRelationship(env.ctx, ownerAccount1, "getcode.com") - require.NoError(t, err) - assert.True(t, allow) - - // Have a set of new relationships that are revoked - for i := 0; i < 10; i++ { - simulateRelationshipEstablished(t, env, ownerAccount1, intent.StateRevoked, time.Now()) - } - - // Limit is unaffected - allow, err = env.guard.AllowEstablishNewRelationship(env.ctx, ownerAccount1, "getcode.com") - require.NoError(t, err) - assert.True(t, allow) - - // Consume the remaining limit - for i := 0; i < 10; i++ { - simulateRelationshipEstablished(t, env, ownerAccount1, testCase, time.Now()) - } - - // New relationships are denied after breaching the daily count limit regardless - // of owner account associated with the phone number - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowEstablishNewRelationship(env.ctx, ownerAccount2, "getcode.com") - require.NoError(t, err) - assert.False(t, allow) - } - } -} - -func TestAllowEstablishNewRelationship_StaffUser(t *testing.T) { - env := setup(t) - - for i, isStaffUser := range []bool{true, false} { - phoneNumber := fmt.Sprintf("+1800555000%d", i) - - ownerAccount1 := testutil.NewRandomAccount(t) - ownerAccount2 := testutil.NewRandomAccount(t) - - require.NoError(t, env.data.PutUser(env.ctx, &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - IsStaffUser: isStaffUser, - CreatedAt: time.Now(), - })) - - for _, ownerAccount := range []*common.Account{ownerAccount1, ownerAccount2} { - verification := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - require.NoError(t, env.guard.data.SavePhoneVerification(env.ctx, verification)) - } - - // Daily limit not consumed - allow, err := env.guard.AllowEstablishNewRelationship(env.ctx, ownerAccount2, "getcode.com") - require.NoError(t, err) - assert.True(t, allow) - - // Consume the remaining limit - for i := 0; i < 10; i++ { - simulateRelationshipEstablished(t, env, ownerAccount1, intent.StatePending, time.Now()) - } - - // Staff users should not be subject to any denials - allow, err = env.guard.AllowEstablishNewRelationship(env.ctx, ownerAccount2, "getcode.com") - require.NoError(t, err) - assert.Equal(t, isStaffUser, allow) - } -} - -func TestAllowNewPhoneVerification_HappyPath(t *testing.T) { - env := setup(t) - - phoneNumber := "+12223334444" - otherPhoneNumber := "+18005550000" - - // New verifications are always allowed when the phone has never started one - // and has a valid device token - for i := 0; i < 5; i++ { - allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.True(t, allow) - } - - // New verifications are always denied when using a fake or unverifiable device. - for i := 0; i < 5; i++ { - allow, reason, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.InvalidDeviceToken)) - require.NoError(t, err) - assert.False(t, allow) - assert.Equal(t, ReasonUnsupportedDevice, reason) - - allow, reason, err = env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, nil) - require.NoError(t, err) - assert.False(t, allow) - assert.Equal(t, ReasonUnsupportedDevice, reason) - } - - // New verifications are allowed when we're under the time interval limit, - // regardless of the number of SMS codes sent within those verifications. - for i := 0; i < 2; i++ { - for j := 0; j < 3; j++ { - simulateSmsCodeSent(t, env, phoneNumber, fmt.Sprintf("verification%d", i)) - - allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.True(t, allow) - } - } - - // New verifications are always denied when the phone breaches the time - // interval limit. - simulateSmsCodeSent(t, env, phoneNumber, "last_allowed_verification") - for i := 0; i < 5; i++ { - allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.False(t, allow) - } - - // Phone numbers are not affected by limits enforced on other phone numbers - allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, otherPhoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken)) - require.NoError(t, err) - assert.True(t, allow) -} - -func TestAllowNewPhoneVerification_StaffUser(t *testing.T) { - env := setup(t) - - phoneNumber := "+12223334444" - - require.NoError(t, env.data.PutUser(env.ctx, &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - IsStaffUser: true, - CreatedAt: time.Now(), - })) - - // Staff users should not be subject to any denials for new verifications - for i := 0; i < 5; i++ { - for j := 0; j < 3; j++ { - simulateSmsCodeSent(t, env, phoneNumber, fmt.Sprintf("verification%d", i)) - - allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, nil) - require.NoError(t, err) - assert.True(t, allow) - } - } -} - -func TestAllowSendSmsVerificationCode(t *testing.T) { - env := setup(t) - - phoneNumber := "+12223334444" - otherPhoneNumber := "+18005550000" - verificationId := "verification" - - // New SMS codes are always allowed to be sent when the phone has never previously sent one - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowSendSmsVerificationCode(env.ctx, phoneNumber) - require.NoError(t, err) - assert.True(t, allow) - } - - // New SMS codes are denied when the minimum time between sends is breached - simulateSmsCodeSent(t, env, phoneNumber, verificationId) - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowSendSmsVerificationCode(env.ctx, phoneNumber) - require.NoError(t, err) - assert.False(t, allow) - } - - // Phone numbers are not affected by limits enforced on other phone numbers - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowSendSmsVerificationCode(env.ctx, otherPhoneNumber) - require.NoError(t, err) - assert.True(t, allow) - } - - // New SMS codes are allowed after waiting the minimum times between sends - // - // todo: need a better way to test with time than waiting - time.Sleep(time.Second) - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowSendSmsVerificationCode(env.ctx, phoneNumber) - require.NoError(t, err) - assert.True(t, allow) - } -} - -func TestAllowCheckSmsVerificationCode(t *testing.T) { - env := setup(t) - - phoneNumber := "+12223334444" - otherPhoneNumber := "+18005550000" - verificationId := "verification" - - // New SMS code checks are always allowed when the phone has never checked one - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowCheckSmsVerificationCode(env.ctx, phoneNumber) - require.NoError(t, err) - assert.True(t, allow) - } - - // New SMS code checks are denied when the minimum time between checks is breached - simulateSmsCodeChecked(t, env, phoneNumber, verificationId) - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowCheckSmsVerificationCode(env.ctx, phoneNumber) - require.NoError(t, err) - assert.False(t, allow) - } - - // Phone numbers are not affected by limits enforced on other phone numbers - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowCheckSmsVerificationCode(env.ctx, otherPhoneNumber) - require.NoError(t, err) - assert.True(t, allow) - } - - // New SMS codes checks are allowed after waiting the minimum times between checks - // - // todo: need a better way to test with time than waiting - time.Sleep(time.Second) - for i := 0; i < 5; i++ { - allow, err := env.guard.AllowCheckSmsVerificationCode(env.ctx, phoneNumber) - require.NoError(t, err) - assert.True(t, allow) - } -} - -func simulateSentPayment(t *testing.T, env testEnv, ownerAccount *common.Account, isPublic bool, state intent.State) { - verificationRecord, err := env.data.GetLatestPhoneVerificationForAccount(env.ctx, ownerAccount.PublicKey().ToBase58()) - require.NoError(t, err) - - if isPublic { - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.SendPublicPayment, - SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ - DestinationOwnerAccount: "destination_owner", - DestinationTokenAccount: "destination_token", - Quantity: 1, - ExchangeCurrency: currency.KIN, - ExchangeRate: 1, - NativeAmount: 1, - UsdMarketValue: 1, - }, - InitiatorOwnerAccount: ownerAccount.PublicKey().ToBase58(), - InitiatorPhoneNumber: &verificationRecord.PhoneNumber, - State: state, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) - } else { - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.SendPrivatePayment, - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - DestinationOwnerAccount: "destination_owner", - DestinationTokenAccount: "destination_token", - Quantity: 1, - ExchangeCurrency: currency.KIN, - ExchangeRate: 1, - NativeAmount: 1, - UsdMarketValue: 1, - }, - InitiatorOwnerAccount: ownerAccount.PublicKey().ToBase58(), - InitiatorPhoneNumber: &verificationRecord.PhoneNumber, - State: state, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) - } -} - -func simulateReceivedPayment(t *testing.T, env testEnv, ownerAccount *common.Account, isPublic bool, state intent.State) { - verificationRecord, err := env.data.GetLatestPhoneVerificationForAccount(env.ctx, ownerAccount.PublicKey().ToBase58()) - require.NoError(t, err) - - if isPublic { - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.ReceivePaymentsPublicly, - ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{ - Source: "gift_card", - Quantity: 1, - IsRemoteSend: true, - OriginalExchangeCurrency: currency.KIN, - OriginalExchangeRate: 1.0, - OriginalNativeAmount: 1.0, - UsdMarketValue: 1, - }, - InitiatorOwnerAccount: ownerAccount.PublicKey().ToBase58(), - InitiatorPhoneNumber: &verificationRecord.PhoneNumber, - State: state, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) - } else { - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.ReceivePaymentsPrivately, - ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{ - Source: "source", - Quantity: 1, - UsdMarketValue: 1, - }, - InitiatorOwnerAccount: ownerAccount.PublicKey().ToBase58(), - InitiatorPhoneNumber: &verificationRecord.PhoneNumber, - State: state, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) - } -} - -func simulateAccountCreation(t *testing.T, env testEnv, ownerAccount *common.Account, state intent.State, createdAt time.Time) { - verificationRecord, err := env.data.GetLatestPhoneVerificationForAccount(env.ctx, ownerAccount.PublicKey().ToBase58()) - require.NoError(t, err) - - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.OpenAccounts, - OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, - InitiatorOwnerAccount: ownerAccount.PublicKey().ToBase58(), - InitiatorPhoneNumber: &verificationRecord.PhoneNumber, - State: state, - CreatedAt: createdAt, - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) -} - -func simulateRelationshipEstablished(t *testing.T, env testEnv, ownerAccount *common.Account, state intent.State, createdAt time.Time) { - verificationRecord, err := env.data.GetLatestPhoneVerificationForAccount(env.ctx, ownerAccount.PublicKey().ToBase58()) - require.NoError(t, err) - - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.EstablishRelationship, - EstablishRelationshipMetadata: &intent.EstablishRelationshipMetadata{ - RelationshipTo: "example.com", - }, - InitiatorOwnerAccount: ownerAccount.PublicKey().ToBase58(), - InitiatorPhoneNumber: &verificationRecord.PhoneNumber, - State: state, - CreatedAt: createdAt, - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) -} - -func simulateSmsCodeSent(t *testing.T, env testEnv, phoneNumber, verification string) { - event := &phone.Event{ - Type: phone.EventTypeVerificationCodeSent, - VerificationId: verification, - PhoneNumber: phoneNumber, - PhoneMetadata: &phone_lib.Metadata{ - PhoneNumber: phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutPhoneEvent(env.ctx, event)) -} - -func simulateSmsCodeChecked(t *testing.T, env testEnv, phoneNumber, verification string) { - event := &phone.Event{ - Type: phone.EventTypeCheckVerificationCode, - VerificationId: verification, - PhoneNumber: phoneNumber, - PhoneMetadata: &phone_lib.Metadata{ - PhoneNumber: phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutPhoneEvent(env.ctx, event)) -} diff --git a/pkg/code/antispam/intent.go b/pkg/code/antispam/intent.go index 4527b23b..977d7f55 100644 --- a/pkg/code/antispam/intent.go +++ b/pkg/code/antispam/intent.go @@ -2,289 +2,26 @@ package antispam import ( "context" - "time" - - "github.com/sirupsen/logrus" "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/grpc/client" "github.com/code-payments/code-server/pkg/metrics" ) // AllowOpenAccounts determines whether a phone-verified owner account can create -// a Code account via an open accounts intent. The objective here is to limit attacks -// against our Subsidizer's SOL balance. -func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, deviceToken *string) (bool, Reason, func() error, error) { +// a Code account via an open accounts intent. +func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account) (bool, error) { tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowOpenAccounts") defer tracer.End() - log := g.log.WithFields(logrus.Fields{ - "method": "AllowOpenAccounts", - "owner": owner.PublicKey().ToBase58(), - }) - log = client.InjectLoggingMetadata(ctx, log) - - // Deny abusers from known IPs - if isIpBanned(ctx) { - log.Info("ip is banned") - recordDenialEvent(ctx, actionOpenAccounts, "ip banned") - return false, ReasonUnspecified, nil, nil - } - - verification, err := g.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - // Owner account was never phone verified, so deny the action. - log.Info("owner account is not phone verified") - recordDenialEvent(ctx, actionOpenAccounts, "not phone verified") - return false, ReasonUnspecified, nil, nil - } else if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting phone verification record") - return false, ReasonUnspecified, nil, err - } - - log = log.WithField("phone", verification.PhoneNumber) - - // Deny users from sanctioned countries - if isSanctionedPhoneNumber(verification.PhoneNumber) { - log.Info("denying sanctioned country") - recordDenialEvent(ctx, actionOpenAccounts, "sanctioned country") - return false, ReasonUnsupportedCountry, nil, nil - } - - user, err := g.data.GetUserByPhoneView(ctx, verification.PhoneNumber) - switch err { - case nil: - // Deny banned users forever - if user.IsBanned { - log.Info("denying banned user") - recordDenialEvent(ctx, actionOpenAccounts, "user banned") - return false, ReasonUnspecified, nil, nil - } - - // Staff users have unlimited access to enable testing and demoing. - if user.IsStaffUser { - return true, ReasonUnspecified, func() error { return nil }, nil - } - case identity.ErrNotFound: - default: - tracer.OnError(err) - log.WithError(err).Warn("failure getting user identity by phone view") - return false, ReasonUnspecified, nil, err - } - - // Account creation limit since the beginning of time - count, err := g.data.GetIntentCountForAntispam( - ctx, - intent.OpenAccounts, - verification.PhoneNumber, - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Unix(0, 0), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, ReasonUnspecified, nil, err - } - - // Device-based restrictions guaranteeing 1 free account per valid device - - _, isAndroidDev := g.conf.androidDevsByPhoneNumber[verification.PhoneNumber] - if !isAndroidDev { - if deviceToken == nil { - log.Info("denying attempt without device token") - recordDenialEvent(ctx, actionOpenAccounts, "device token missing") - return false, ReasonUnsupportedDevice, nil, nil - } - - isValidDeviceToken, reason, err := g.deviceVerifier.IsValid(ctx, *deviceToken) - if err != nil { - log.WithError(err).Warn("failure performing device validation check") - return false, ReasonUnspecified, nil, err - } else if !isValidDeviceToken { - log.WithField("reason", reason).Info("denying fake device") - recordDenialEvent(ctx, actionOpenAccounts, "fake device") - return false, ReasonUnsupportedDevice, nil, nil - } - - hasCreatedFreeAccount, err := g.deviceVerifier.HasCreatedFreeAccount(ctx, *deviceToken) - if err != nil { - log.WithError(err).Warn("failure performing free account check for device") - return false, ReasonUnspecified, nil, err - } else if hasCreatedFreeAccount { - log.Info("denying duplicate device") - recordDenialEvent(ctx, actionOpenAccounts, "duplicate device") - return false, ReasonTooManyFreeAccountsForDevice, nil, nil - } - } - - // Note: If we have multiple accounts per phone number, then this affects the - // incenvtives logic, which uses this assumption. - if int(count) >= 1 { - log.Info("phone is rate limited by lifetime account creation count") - recordDenialEvent(ctx, actionOpenAccounts, "lifetime limit exceeded") - return false, ReasonTooManyFreeAccountsForPhoneNumber, nil, nil - } - - onFreeAccountCreated := func() error { - if isAndroidDev { - return nil - } - return g.deviceVerifier.MarkCreatedFreeAccount(ctx, *deviceToken) - } - return true, ReasonUnspecified, onFreeAccountCreated, nil + return true, nil } // AllowSendPayment determines whether a phone-verified owner account is allowed to -// make a send public/private payment intent. The objective is to limit pressure on -// the scheduling layer. +// make a send public/private payment intent. func (g *Guard) AllowSendPayment(ctx context.Context, owner *common.Account, isPublic bool, destination *common.Account) (bool, error) { tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowSendPayment") defer tracer.End() - log := g.log.WithFields(logrus.Fields{ - "method": "AllowSendPayment", - "owner": owner.PublicKey().ToBase58(), - }) - log = client.InjectLoggingMetadata(ctx, log) - - // Deny abusers from known IPs - if isIpBanned(ctx) { - log.Info("ip is banned") - recordDenialEvent(ctx, actionSendPayment, "ip banned") - return false, nil - } - - if isPublic && isExternalAddressBanned(destination) { - log.WithField("address", destination.PublicKey().ToBase58()).Info("external address is banned") - recordDenialEvent(ctx, actionSendPayment, "external address banned") - return false, nil - } - - verification, err := g.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - // Owner account was never phone verified, so deny the action. - log.Info("owner account is not phone verified") - recordDenialEvent(ctx, actionSendPayment, "not phone verified") - return false, nil - } else if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting phone verification record") - return false, err - } - - log = log.WithField("phone", verification.PhoneNumber) - - // Deny users from sanctioned countries - if isSanctionedPhoneNumber(verification.PhoneNumber) { - log.Info("denying sanctioned country") - recordDenialEvent(ctx, actionSendPayment, "sanctioned country") - return false, nil - } - - user, err := g.data.GetUserByPhoneView(ctx, verification.PhoneNumber) - switch err { - case nil: - // Deny banned users forever - if user.IsBanned { - log.Info("denying banned user") - recordDenialEvent(ctx, actionSendPayment, "user banned") - return false, nil - } - - // Staff users have unlimited access to enable testing and demoing. - if user.IsStaffUser { - return true, nil - } - case identity.ErrNotFound: - default: - tracer.OnError(err) - log.WithError(err).Warn("failure getting user identity by phone view") - return false, err - } - - // Time-based rate limit per phone number across all sends - rateLimited, err := g.limiter.denyPaymentByPhone(verification.PhoneNumber) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure checking phone rate limit") - return false, err - } else if rateLimited { - log.Info("phone is rate limited by time") - recordDenialEvent(ctx, actionSendPayment, "rate limit exceeded") - return false, nil - } - - // Count limits are applied separately for public and private payments. Each - // has their own costs. - intentType := intent.SendPrivatePayment - if isPublic { - intentType = intent.SendPublicPayment - } - - // Five minute payment limit by phone number - count, err := g.data.GetIntentCountForAntispam( - ctx, - intentType, - verification.PhoneNumber, - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Now().Add(-5*time.Minute), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, err - } - - if count >= g.conf.paymentsPerFiveMinutes { - log.Info("phone is rate limited by five minute payment count") - recordDenialEvent(ctx, actionSendPayment, "five minute limit exceeded") - return false, nil - } - - // Hourly payment limit by phone number - count, err = g.data.GetIntentCountForAntispam( - ctx, - intentType, - verification.PhoneNumber, - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Now().Add(-1*time.Hour), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, err - } - - if count >= g.conf.paymentsPerHour { - log.Info("phone is rate limited by hourly payment count") - recordDenialEvent(ctx, actionSendPayment, "hourly limit exceeded") - return false, nil - } - - // Daily payment limit by phone number - count, err = g.data.GetIntentCountForAntispam( - ctx, - intentType, - verification.PhoneNumber, - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Now().Add(-24*time.Hour), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, err - } - - if count >= g.conf.paymentsPerDay { - log.Info("phone is rate limited by daily payment count") - recordDenialEvent(ctx, actionSendPayment, "daily limit exceeded") - return false, nil - } - return true, nil } @@ -295,224 +32,5 @@ func (g *Guard) AllowReceivePayments(ctx context.Context, owner *common.Account, tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowReceivePayments") defer tracer.End() - log := g.log.WithFields(logrus.Fields{ - "method": "AllowReceivePayments", - "owner": owner.PublicKey().ToBase58(), - }) - log = client.InjectLoggingMetadata(ctx, log) - - // Deny abusers from known IPs - if isIpBanned(ctx) { - log.Info("ip is banned") - recordDenialEvent(ctx, actionReceivePayments, "ip banned") - return false, nil - } - - verification, err := g.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - // Owner account was never phone verified, so deny the action. - log.Info("owner account is not phone verified") - recordDenialEvent(ctx, actionReceivePayments, "not phone verified") - return false, nil - } else if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting phone verification record") - return false, err - } - - log = log.WithField("phone", verification.PhoneNumber) - - // Deny users from sanctioned countries - if isSanctionedPhoneNumber(verification.PhoneNumber) { - log.Info("denying sanctioned country") - recordDenialEvent(ctx, actionReceivePayments, "sanctioned country") - return false, nil - } - - user, err := g.data.GetUserByPhoneView(ctx, verification.PhoneNumber) - switch err { - case nil: - // Deny banned users forever - if user.IsBanned { - log.Info("denying banned user") - recordDenialEvent(ctx, actionReceivePayments, "user banned") - return false, nil - } - - // Staff users have unlimited access to enable testing and demoing. - if user.IsStaffUser { - return true, nil - } - case identity.ErrNotFound: - default: - tracer.OnError(err) - log.WithError(err).Warn("failure getting user identity by phone view") - return false, err - } - - // Time-based rate limit per phone number - rateLimited, err := g.limiter.denyPaymentByPhone(verification.PhoneNumber) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure checking phone rate limit") - return false, err - } else if rateLimited { - log.Info("phone is rate limited by time") - recordDenialEvent(ctx, actionReceivePayments, "rate limit exceeded") - return false, nil - } - - // Count limits are applied separately for public and private payments. Each - // has their own costs. - intentType := intent.ReceivePaymentsPrivately - if isPublic { - intentType = intent.ReceivePaymentsPublicly - } - - // Five minute payment limit by phone number - count, err := g.data.GetIntentCountForAntispam( - ctx, - intentType, - verification.PhoneNumber, - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Now().Add(-5*time.Minute), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, err - } - - if count >= g.conf.paymentsPerFiveMinutes { - log.Info("phone is rate limited by five minute payment count") - recordDenialEvent(ctx, actionReceivePayments, "five minute limit exceeded") - return false, nil - } - - // Hourly payment limit by phone number - count, err = g.data.GetIntentCountForAntispam( - ctx, - intentType, - verification.PhoneNumber, - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Now().Add(-1*time.Hour), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, err - } - - if count >= g.conf.paymentsPerHour { - log.Info("phone is rate limited by hourly payment count") - recordDenialEvent(ctx, actionReceivePayments, "hourly limit exceeded") - return false, nil - } - - // Daily payment limit by phone number - count, err = g.data.GetIntentCountForAntispam( - ctx, - intentType, - verification.PhoneNumber, - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Now().Add(-24*time.Hour), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, err - } - - if count >= g.conf.paymentsPerDay { - log.Info("phone is rate limited by daily payment count") - recordDenialEvent(ctx, actionReceivePayments, "daily limit exceeded") - return false, nil - } - - return true, nil -} - -// AllowEstablishNewRelationship determines whether a phone-verified owner account is allowed -// to establish a new relationship -func (g *Guard) AllowEstablishNewRelationship(ctx context.Context, owner *common.Account, relationshipTo string) (bool, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowEstablishNewRelationship") - defer tracer.End() - - log := g.log.WithFields(logrus.Fields{ - "method": "AllowEstablishNewRelationship", - "owner": owner.PublicKey().ToBase58(), - "relationship_to": relationshipTo, - }) - log = client.InjectLoggingMetadata(ctx, log) - - // Deny abusers from known IPs - if isIpBanned(ctx) { - log.Info("ip is banned") - recordDenialEvent(ctx, actionEstablishNewRelationship, "ip banned") - return false, nil - } - - verification, err := g.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - // Owner account was never phone verified, so deny the action. - log.Info("owner account is not phone verified") - recordDenialEvent(ctx, actionEstablishNewRelationship, "not phone verified") - return false, nil - } else if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting phone verification record") - return false, err - } - - log = log.WithField("phone", verification.PhoneNumber) - - // Deny users from sanctioned countries - if isSanctionedPhoneNumber(verification.PhoneNumber) { - log.Info("denying sanctioned country") - recordDenialEvent(ctx, actionEstablishNewRelationship, "sanctioned country") - return false, nil - } - - user, err := g.data.GetUserByPhoneView(ctx, verification.PhoneNumber) - switch err { - case nil: - // Deny banned users forever - if user.IsBanned { - log.Info("denying banned user") - recordDenialEvent(ctx, actionEstablishNewRelationship, "user banned") - return false, nil - } - - // Staff users have unlimited access to enable testing and demoing. - if user.IsStaffUser { - return true, nil - } - case identity.ErrNotFound: - default: - tracer.OnError(err) - log.WithError(err).Warn("failure getting user identity by phone view") - return false, err - } - - // Basic rate limit across the last day - count, err := g.data.GetIntentCountForAntispam( - ctx, - intent.EstablishRelationship, - verification.PhoneNumber, - []intent.State{intent.StateUnknown, intent.StatePending, intent.StateFailed, intent.StateConfirmed}, - time.Now().Add(-24*time.Hour), - ) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting intent count") - return false, err - } - - if count >= g.conf.maxNewRelationshipsPerDay { - log.Info("phone is rate limited by daily count") - recordDenialEvent(ctx, actionEstablishNewRelationship, "daily limit exceeded") - return false, nil - } - return true, nil } diff --git a/pkg/code/antispam/ip.go b/pkg/code/antispam/ip.go deleted file mode 100644 index bd355c74..00000000 --- a/pkg/code/antispam/ip.go +++ /dev/null @@ -1,11 +0,0 @@ -package antispam - -import ( - "context" -) - -// This isn't generally very useful (at least for incentive spammers). Let's -// keep it clean for now. -func isIpBanned(ctx context.Context) bool { - return false -} diff --git a/pkg/code/antispam/limiter.go b/pkg/code/antispam/limiter.go deleted file mode 100644 index 02199317..00000000 --- a/pkg/code/antispam/limiter.go +++ /dev/null @@ -1,24 +0,0 @@ -package antispam - -import ( - "github.com/code-payments/code-server/pkg/rate" -) - -type limiter struct { - paymentsByPhone rate.Limiter -} - -func newLimiter(ctor rate.LimiterCtor, paymentsByPhoneLimit float64) *limiter { - return &limiter{ - paymentsByPhone: ctor(paymentsByPhoneLimit), - } -} - -func (l *limiter) denyPaymentByPhone(phoneNumber string) (bool, error) { - allowed, err := l.paymentsByPhone.Allow(phoneNumber) - if err != nil { - // Being extra safe in the event of failure. Do not allow anything. - return true, err - } - return !allowed, nil -} diff --git a/pkg/code/antispam/metrics.go b/pkg/code/antispam/metrics.go index d864010b..8373d8a4 100644 --- a/pkg/code/antispam/metrics.go +++ b/pkg/code/antispam/metrics.go @@ -11,15 +11,9 @@ const ( eventName = "AntispamGuardDenial" - actionOpenAccounts = "OpenAccounts" - actionSendPayment = "SendPayment" - actionReceivePayments = "ReceivePayments" - actionEstablishNewRelationship = "EstablishNewRelationship" - - actionNewPhoneVerification = "NewPhoneVerification" - actionSendSmsVerificationCode = "SendSmsVerificationCode" - actionCheckSmsVerificationCode = "CheckSmsVerificationCode" - actionLinkAccount = "LinkAccount" + actionOpenAccounts = "OpenAccounts" + actionSendPayment = "SendPayment" + actionReceivePayments = "ReceivePayments" actionWelcomeBonus = "WelcomeBonus" actionReferralBonus = "ReferralBonus" diff --git a/pkg/code/antispam/phone.go b/pkg/code/antispam/phone.go deleted file mode 100644 index f5fbeeca..00000000 --- a/pkg/code/antispam/phone.go +++ /dev/null @@ -1,30 +0,0 @@ -package antispam - -import "strings" - -// todo: Put in a DB somehwere? Or make configurable? -// todo: Needs tests -func isSanctionedPhoneNumber(phoneNumber string) bool { - // Check that a +7 phone number is not a mobile number from Kazakhstan - if strings.HasPrefix(phoneNumber, "+76") || strings.HasPrefix(phoneNumber, "+77") { - return false - } - - // Sanctioned countries - // - // todo: Probably doesn't belong in an antispam package, but it's just a - // convenient place for now - for _, prefix := range []string{ - "+53", // Cuba - "+98", // Iran - "+850", // North Korea - "+7", // Russia - "+963", // Syria - "+58", // Venezuala - } { - if strings.HasPrefix(phoneNumber, prefix) { - return true - } - } - return false -} diff --git a/pkg/code/antispam/phone_verification.go b/pkg/code/antispam/phone_verification.go deleted file mode 100644 index 7f61724b..00000000 --- a/pkg/code/antispam/phone_verification.go +++ /dev/null @@ -1,187 +0,0 @@ -package antispam - -import ( - "context" - "time" - - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/metrics" -) - -// AllowNewPhoneVerification determines whether a phone is allowed to start a -// new verification flow. -func (g *Guard) AllowNewPhoneVerification(ctx context.Context, phoneNumber string, deviceToken *string) (bool, Reason, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowNewPhoneVerification") - defer tracer.End() - - log := g.log.WithFields(logrus.Fields{ - "method": "AllowNewPhoneVerification", - "phone_number": phoneNumber, - }) - log = client.InjectLoggingMetadata(ctx, log) - - // Deny abusers from known IPs - if isIpBanned(ctx) { - log.Info("ip is banned") - recordDenialEvent(ctx, actionNewPhoneVerification, "ip banned") - return false, ReasonUnspecified, nil - } - - // Deny users from sanctioned countries - if isSanctionedPhoneNumber(phoneNumber) { - log.Info("denying sanctioned country") - recordDenialEvent(ctx, actionNewPhoneVerification, "sanctioned country") - return false, ReasonUnsupportedCountry, nil - } - - user, err := g.data.GetUserByPhoneView(ctx, phoneNumber) - switch err { - case nil: - // Deny banned users forever - if user.IsBanned { - log.Info("denying banned user") - recordDenialEvent(ctx, actionNewPhoneVerification, "user banned") - return false, ReasonUnspecified, nil - } - - // Staff users have unlimited access to enable testing and demoing. - if user.IsStaffUser { - return true, ReasonUnspecified, nil - } - case identity.ErrNotFound: - default: - tracer.OnError(err) - log.WithError(err).Warn("failure getting user identity by phone view") - return false, ReasonUnspecified, err - } - - _, isAndroidDev := g.conf.androidDevsByPhoneNumber[phoneNumber] - if !isAndroidDev { - // Importantly, after staff checks so we don't gate testing on devices where - // device tokens don't exist (eg. iOS simulator) - if deviceToken == nil { - log.Info("denying attempt without device token") - recordDenialEvent(ctx, actionNewPhoneVerification, "device token missing") - return false, ReasonUnsupportedDevice, nil - } - isValidDeviceToken, reason, err := g.deviceVerifier.IsValid(ctx, *deviceToken) - if err != nil { - log.WithError(err).Warn("failure performing device check") - return false, ReasonUnspecified, err - } else if !isValidDeviceToken { - log.WithField("reason", reason).Info("denying fake device") - recordDenialEvent(ctx, actionNewPhoneVerification, "fake device") - return false, ReasonUnsupportedDevice, nil - } - } - - since := time.Now().Add(-1 * g.conf.phoneVerificationInterval) - count, err := g.data.GetUniquePhoneVerificationIdCountForNumberSinceTimestamp(ctx, phoneNumber, since) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure counting unique verification ids for number") - return false, ReasonUnspecified, err - } - - if count >= g.conf.phoneVerificationsPerInternval { - log.Info("phone is rate limited") - recordDenialEvent(ctx, actionNewPhoneVerification, "rate limit exceeded") - return false, ReasonUnspecified, nil - } - return true, ReasonUnspecified, nil -} - -// AllowSendSmsVerificationCode determines whether a phone number can be sent -// a verification code over SMS. -func (g *Guard) AllowSendSmsVerificationCode(ctx context.Context, phoneNumber string) (bool, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowSendSmsVerificationCode") - defer tracer.End() - - log := g.log.WithFields(logrus.Fields{ - "method": "AllowSendSmsVerificationCode", - "phone_number": phoneNumber, - }) - - since := time.Now().Add(-1 * g.conf.timePerSmsVerificationCodeSend) - count, err := g.data.GetPhoneEventCountForNumberByTypeSinceTimestamp(ctx, phoneNumber, phone.EventTypeVerificationCodeSent, since) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure counting phone events") - return false, err - } - - if count > 0 { - log.Info("phone is rate limited") - recordDenialEvent(ctx, actionSendSmsVerificationCode, "rate limit exceeded") - return false, nil - } - return true, nil -} - -// AllowCheckSmsVerificationCode determines whether a phone number is allowed -// to check a SMS verification code. -func (g *Guard) AllowCheckSmsVerificationCode(ctx context.Context, phoneNumber string) (bool, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowCheckSmsVerificationCode") - defer tracer.End() - - log := g.log.WithFields(logrus.Fields{ - "method": "AllowCheckSmsVerificationCode", - "phone_number": phoneNumber, - }) - log = client.InjectLoggingMetadata(ctx, log) - - since := time.Now().Add(-1 * g.conf.timePerSmsVerificationCodeCheck) - count, err := g.data.GetPhoneEventCountForNumberByTypeSinceTimestamp(ctx, phoneNumber, phone.EventTypeCheckVerificationCode, since) - if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure counting phone events") - return false, err - } - - if count > 0 { - log.Info("phone is rate limited") - recordDenialEvent(ctx, actionCheckSmsVerificationCode, "rate limit exceeded") - return false, nil - } - return true, nil -} - -// AllowLinkAccount determines whether an identity is allowed to link to an -// account. -// -// todo: this needs tests -func (g *Guard) AllowLinkAccount(ctx context.Context, ownerAccount *common.Account, phoneNumber string) (bool, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowLinkAccount") - defer tracer.End() - - log := g.log.WithFields(logrus.Fields{ - "method": "AllowLinkAccount", - "owner_account": ownerAccount.PublicKey().ToBase58(), - "phone_number": phoneNumber, - }) - log = client.InjectLoggingMetadata(ctx, log) - - previousPhoneVerificationRecord, err := g.data.GetLatestPhoneVerificationForAccount(ctx, ownerAccount.PublicKey().ToBase58()) - if err == nil { - log = log.WithField("previous_phone_number", previousPhoneVerificationRecord.PhoneNumber) - - // Don't allow new links when the previous one is to a banned user. - previousUserRecord, err := g.data.GetUserByPhoneView(ctx, previousPhoneVerificationRecord.PhoneNumber) - if err == nil && previousUserRecord.IsBanned { - log.Info("denying relink where previous user is banned") - recordDenialEvent(ctx, actionLinkAccount, "previous user banned") - return false, nil - } - } else if err != phone.ErrVerificationNotFound { - tracer.OnError(err) - log.WithError(err).Warn("failure getting previous phone verification record") - return false, err - } - - return true, nil -} diff --git a/pkg/code/antispam/reason.go b/pkg/code/antispam/reason.go deleted file mode 100644 index 36959722..00000000 --- a/pkg/code/antispam/reason.go +++ /dev/null @@ -1,11 +0,0 @@ -package antispam - -type Reason int - -const ( - ReasonUnspecified Reason = iota - ReasonUnsupportedCountry - ReasonUnsupportedDevice - ReasonTooManyFreeAccountsForPhoneNumber - ReasonTooManyFreeAccountsForDevice -) diff --git a/pkg/code/antispam/swap.go b/pkg/code/antispam/swap.go index fd0fd1e3..a2fd59b0 100644 --- a/pkg/code/antispam/swap.go +++ b/pkg/code/antispam/swap.go @@ -3,77 +3,15 @@ package antispam import ( "context" - "github.com/sirupsen/logrus" - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/grpc/client" "github.com/code-payments/code-server/pkg/metrics" ) // AllowSwap determines whether a phone-verified owner account can perform a swap. // The objective here is to limit attacks against our Swap Subsidizer's SOL balance. -// -// todo: needs tests func (g *Guard) AllowSwap(ctx context.Context, owner *common.Account) (bool, error) { tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowSwap") defer tracer.End() - log := g.log.WithFields(logrus.Fields{ - "method": "AllowSwap", - "owner": owner.PublicKey().ToBase58(), - }) - log = client.InjectLoggingMetadata(ctx, log) - - // Deny abusers from known IPs - if isIpBanned(ctx) { - log.Info("ip is banned") - recordDenialEvent(ctx, actionSwap, "ip banned") - return false, nil - } - - verification, err := g.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - // Owner account was never phone verified, so deny the action. - log.Info("owner account is not phone verified") - recordDenialEvent(ctx, actionSwap, "not phone verified") - return false, nil - } else if err != nil { - tracer.OnError(err) - log.WithError(err).Warn("failure getting phone verification record") - return false, err - } - - log = log.WithField("phone", verification.PhoneNumber) - - // Deny users from sanctioned countries - if isSanctionedPhoneNumber(verification.PhoneNumber) { - log.Info("denying sanctioned country") - recordDenialEvent(ctx, actionSwap, "sanctioned country") - return false, nil - } - - user, err := g.data.GetUserByPhoneView(ctx, verification.PhoneNumber) - switch err { - case nil: - // Deny banned users forever - if user.IsBanned { - log.Info("denying banned user") - recordDenialEvent(ctx, actionSwap, "user banned") - return false, nil - } - - // Staff users have unlimited access to enable testing and demoing. - if user.IsStaffUser { - return true, nil - } - case identity.ErrNotFound: - default: - tracer.OnError(err) - log.WithError(err).Warn("failure getting user identity by phone view") - return false, err - } - return true, nil } diff --git a/pkg/code/async/account/gift_card.go b/pkg/code/async/account/gift_card.go index 4ad292dc..ed6fd6eb 100644 --- a/pkg/code/async/account/gift_card.go +++ b/pkg/code/async/account/gift_card.go @@ -2,30 +2,20 @@ package async_account import ( "context" - "crypto/sha256" "errors" - "math" "sync" "time" - "github.com/mr-tron/base58" "github.com/newrelic/go-agent/v3/newrelic" "github.com/sirupsen/logrus" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/push" - "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/retry" ) @@ -138,76 +128,67 @@ func (p *service) maybeInitiateGiftCardAutoReturn(ctx context.Context, accountIn // Note: This is the first instance of handling a conditional action, and could be // a good guide for similar actions in the future. func (p *service) initiateProcessToAutoReturnGiftCard(ctx context.Context, giftCardVaultAccount *common.Account) error { - giftCardIssuedIntent, err := p.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVaultAccount.PublicKey().ToBase58()) - if err != nil { - return err - } - - autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) - if err != nil { - return err - } - - autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId) - if err != nil { - return err - } - - // Add a payment history item to show the funds being returned back to the issuer - err = insertAutoReturnPaymentHistoryItem(ctx, p.data, giftCardIssuedIntent) - if err != nil { - return err - } - - // We need to update pre-sorting because close dormant fulfillments are always - // inserted at the very last spot in the line. - // - // Must be the first thing to succeed! We cannot risk a deposit back into the - // organizer to win a race in scheduling. By pre-sorting this to the end of - // the gift card issued intent, we ensure the auto-return is blocked on any - // fulfillments to setup the gift card. We'll also guarantee that subsequent - // intents that utilize the primary account as a source of funds will be blocked - // by the auto-return. - err = updateCloseDormantAccountFulfillmentPreSorting( - ctx, - p.data, - autoReturnFulfillment[0], - giftCardIssuedIntent.Id, - math.MaxInt32, - 0, - ) - if err != nil { - return err - } - - // This will update the action's quantity, so balance changes are reflected. We - // also unblock fulfillment scheduling by moving the action out of the unknown - // state and into the pending state. - err = scheduleCloseDormantAccountAction( - ctx, - p.data, - autoReturnAction, - giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity, - ) - if err != nil { - return err - } - - // This will trigger the fulfillment worker to poll for the fulfillment. This - // should be the very last DB update called. - err = markFulfillmentAsActivelyScheduled(ctx, p.data, autoReturnFulfillment[0]) - if err != nil { - return err - } - - // Finally, update the user by best-effort sending them a push - go push.SendGiftCardReturnedPushNotification( - ctx, - p.data, - p.pusher, - giftCardVaultAccount, - ) - return nil + /* + giftCardIssuedIntent, err := p.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err != nil { + return err + } + + autoReturnAction, err := p.data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err != nil { + return err + } + + autoReturnFulfillment, err := p.data.GetAllFulfillmentsByAction(ctx, autoReturnAction.Intent, autoReturnAction.ActionId) + if err != nil { + return err + } + + // Add a payment history item to show the funds being returned back to the issuer + err = insertAutoReturnPaymentHistoryItem(ctx, p.data, giftCardIssuedIntent) + if err != nil { + return err + } + + // We need to update pre-sorting because close dormant fulfillments are always + // inserted at the very last spot in the line. + // + // Must be the first thing to succeed! We cannot risk a deposit back into the + // organizer to win a race in scheduling. By pre-sorting this to the end of + // the gift card issued intent, we ensure the auto-return is blocked on any + // fulfillments to setup the gift card. We'll also guarantee that subsequent + // intents that utilize the primary account as a source of funds will be blocked + // by the auto-return. + err = updateCloseDormantAccountFulfillmentPreSorting( + ctx, + p.data, + autoReturnFulfillment[0], + giftCardIssuedIntent.Id, + math.MaxInt32, + 0, + ) + if err != nil { + return err + } + + // This will update the action's quantity, so balance changes are reflected. We + // also unblock fulfillment scheduling by moving the action out of the unknown + // state and into the pending state. + err = scheduleCloseDormantAccountAction( + ctx, + p.data, + autoReturnAction, + giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity, + ) + if err != nil { + return err + } + + // This will trigger the fulfillment worker to poll for the fulfillment. This + // should be the very last DB update called. + return markFulfillmentAsActivelyScheduled(ctx, p.data, autoReturnFulfillment[0]) + */ + return errors.New("requires rewrite") } func markAutoReturnCheckComplete(ctx context.Context, data code_data.Provider, record *account.Record) error { @@ -219,6 +200,8 @@ func markAutoReturnCheckComplete(ctx context.Context, data code_data.Provider, r return data.UpdateAccountInfo(ctx, record) } +/* + // Note: Structured like a generic utility because it could very well evolve // into that, but there's no reason to call this on anything else as of // writing this comment. @@ -308,7 +291,6 @@ func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Prov IntentType: intent.ReceivePaymentsPublicly, InitiatorOwnerAccount: giftCardIssuedIntent.InitiatorOwnerAccount, - InitiatorPhoneNumber: giftCardIssuedIntent.InitiatorPhoneNumber, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{ Source: giftCardIssuedIntent.SendPrivatePaymentMetadata.DestinationTokenAccount, @@ -320,19 +302,14 @@ func insertAutoReturnPaymentHistoryItem(ctx context.Context, data code_data.Prov OriginalExchangeRate: giftCardIssuedIntent.SendPrivatePaymentMetadata.ExchangeRate, OriginalNativeAmount: giftCardIssuedIntent.SendPrivatePaymentMetadata.NativeAmount, - UsdMarketValue: usdExchangeRecord.Rate * float64(kin.FromQuarks(giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity)), + UsdMarketValue: usdExchangeRecord.Rate * float64(common.FromCoreMintQuarks(giftCardIssuedIntent.SendPrivatePaymentMetadata.Quantity)), }, State: intent.StateConfirmed, CreatedAt: time.Now(), } - err = data.SaveIntent(ctx, intentRecord) - if err != nil { - return err - } - - return chat_util.SendCashTransactionsExchangeMessage(ctx, data, intentRecord) + return data.SaveIntent(ctx, intentRecord) } // Must be unique, but consistent for idempotency, and ideally fit in a 32 @@ -341,3 +318,4 @@ func getAutoReturnIntentId(originalIntentId string) string { hashed := sha256.Sum256([]byte(giftCardAutoReturnIntentPrefix + originalIntentId)) return base58.Encode(hashed[:]) } +*/ diff --git a/pkg/code/async/account/gift_card_test.go b/pkg/code/async/account/gift_card_test.go index 51636bb8..d380dcc2 100644 --- a/pkg/code/async/account/gift_card_test.go +++ b/pkg/code/async/account/gift_card_test.go @@ -1,15 +1,6 @@ package async_account -import ( - "testing" - "time" - - "github.com/mr-tron/base58/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/testutil" -) +/* func TestGiftCardAutoReturn_ExpiryWindow(t *testing.T) { for _, tc := range []struct { @@ -85,3 +76,5 @@ func TestGiftCardAutoReturn_IntentId(t *testing.T) { assert.Equal(t, generated2, getAutoReturnIntentId(intentId2)) } } + +*/ diff --git a/pkg/code/async/account/service.go b/pkg/code/async/account/service.go index a8ef5c6b..bed46ed9 100644 --- a/pkg/code/async/account/service.go +++ b/pkg/code/async/account/service.go @@ -8,22 +8,19 @@ import ( "github.com/code-payments/code-server/pkg/code/async" code_data "github.com/code-payments/code-server/pkg/code/data" - push_lib "github.com/code-payments/code-server/pkg/push" ) type service struct { - log *logrus.Entry - conf *conf - data code_data.Provider - pusher push_lib.Provider + log *logrus.Entry + conf *conf + data code_data.Provider } -func New(data code_data.Provider, pusher push_lib.Provider, configProvider ConfigProvider) async.Service { +func New(data code_data.Provider, configProvider ConfigProvider) async.Service { return &service{ - log: logrus.StandardLogger().WithField("service", "account"), - conf: configProvider(), - data: data, - pusher: pusher, + log: logrus.StandardLogger().WithField("service", "account"), + conf: configProvider(), + data: data, } } @@ -38,12 +35,15 @@ func (p *service) Start(ctx context.Context, interval time.Duration) error { }() */ - go func() { - err := p.swapRetryWorker(ctx, interval) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("swap retry processing loop terminated unexpectedly") - } - }() + // todo: the open code protocol needs to get the push token from the implementing app + /* + go func() { + err := p.swapRetryWorker(ctx, interval) + if err != nil && err != context.Canceled { + p.log.WithError(err).Warn("swap retry processing loop terminated unexpectedly") + } + }() + */ go func() { err := p.metricsGaugeWorker(ctx) diff --git a/pkg/code/async/account/swap.go b/pkg/code/async/account/swap.go index 8c80f70e..31ed5bb2 100644 --- a/pkg/code/async/account/swap.go +++ b/pkg/code/async/account/swap.go @@ -1,5 +1,6 @@ package async_account +/* import ( "context" "errors" @@ -143,3 +144,4 @@ func markSwapRetriedNow(ctx context.Context, data code_data.Provider, record *ac record.LastSwapRetryAt = time.Now() return data.UpdateAccountInfo(ctx, record) } +*/ diff --git a/pkg/code/async/account/testutil.go b/pkg/code/async/account/testutil.go index 19f285ca..2e109276 100644 --- a/pkg/code/async/account/testutil.go +++ b/pkg/code/async/account/testutil.go @@ -1,5 +1,6 @@ package async_account +/* import ( "context" "math" @@ -18,10 +19,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/currency" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" - memory_push "github.com/code-payments/code-server/pkg/push/memory" "github.com/code-payments/code-server/pkg/testutil" ) @@ -55,7 +53,7 @@ func setup(t *testing.T) *testEnv { return &testEnv{ ctx: context.Background(), data: data, - service: New(data, memory_push.NewPushProvider(), WithEnvConfigs()).(*service), + service: New(data, WithEnvConfigs()).(*service), } } @@ -63,7 +61,7 @@ func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *te vm := testutil.NewRandomAccount(t) authority := testutil.NewRandomAccount(t) - timelockAccounts, err := authority.GetTimelockAccounts(vm, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(vm, common.CoreMintAccount) require.NoError(t, err) accountInfoRecord := &account.Record{ @@ -85,13 +83,12 @@ func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *te IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - InitiatorPhoneNumber: pointer.String("+12223334444"), SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ DestinationTokenAccount: accountInfoRecord.TokenAccount, - Quantity: kin.ToQuarks(12345), + Quantity: common.ToCoreMintQuarks(12345), - ExchangeCurrency: currency_lib.KIN, + ExchangeCurrency: common.CoreMintSymbol, ExchangeRate: 1.0, NativeAmount: 12345, UsdMarketValue: 1000.0, @@ -204,7 +201,6 @@ func (e *testEnv) assertGiftCardAutoReturned(t *testing.T, giftCard *testGiftCar assert.Equal(t, intentId, historyRecord.IntentId) assert.Equal(t, intent.ReceivePaymentsPublicly, historyRecord.IntentType) assert.Equal(t, giftCard.issuedIntentRecord.InitiatorOwnerAccount, historyRecord.InitiatorOwnerAccount) - assert.EqualValues(t, giftCard.issuedIntentRecord.InitiatorPhoneNumber, historyRecord.InitiatorPhoneNumber) require.NotNil(t, historyRecord.ReceivePaymentsPubliclyMetadata) assert.Equal(t, giftCard.accountInfoRecord.TokenAccount, historyRecord.ReceivePaymentsPubliclyMetadata.Source) assert.Equal(t, giftCard.issuedIntentRecord.SendPrivatePaymentMetadata.Quantity, historyRecord.ReceivePaymentsPubliclyMetadata.Quantity) @@ -238,3 +234,4 @@ func (e *testEnv) assertGiftCardNotAutoReturned(t *testing.T, giftCard *testGift _, err = e.data.GetIntent(e.ctx, giftCardAutoReturnIntentPrefix+giftCard.issuedIntentRecord.IntentId) assert.Equal(t, intent.ErrIntentNotFound, err) } +*/ diff --git a/pkg/code/async/airdrop/service.go b/pkg/code/async/airdrop/service.go index 68472239..a16257d5 100644 --- a/pkg/code/async/airdrop/service.go +++ b/pkg/code/async/airdrop/service.go @@ -86,7 +86,7 @@ func (p *service) loadAirdropper(ctx context.Context) error { return err } - p.airdropperTimelockAccounts, err = p.airdropper.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + p.airdropperTimelockAccounts, err = p.airdropper.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) if err != nil { log.WithError(err).Warn("failed to dervice timelock accounts") return err diff --git a/pkg/code/async/airdrop/transaction.go b/pkg/code/async/airdrop/transaction.go index 12558221..e060585a 100644 --- a/pkg/code/async/airdrop/transaction.go +++ b/pkg/code/async/airdrop/transaction.go @@ -214,7 +214,7 @@ func (p *service) onSuccess(ctx context.Context, txn *solana.ConfirmedTransactio var vaults []*common.Account for _, owner := range owners { - timelockAccounts, err := owner.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) if err != nil { return err } diff --git a/pkg/code/async/commitment/merkle_tree.go b/pkg/code/async/commitment/merkle_tree.go deleted file mode 100644 index 14b1cbe5..00000000 --- a/pkg/code/async/commitment/merkle_tree.go +++ /dev/null @@ -1,70 +0,0 @@ -package async_commitment - -import ( - "context" - "sync" - "time" - - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/merkletree" -) - -type refreshingMerkleTree struct { - tree *merkletree.MerkleTree - lastRefreshedAt time.Time -} - -var ( - merkleTreeLock sync.Mutex - treasuryPoolAddressToName map[string]string - cachedMerkleTrees map[string]*refreshingMerkleTree -) - -func init() { - treasuryPoolAddressToName = make(map[string]string) - cachedMerkleTrees = make(map[string]*refreshingMerkleTree) -} - -// todo: move this into a common spot? code is duplicated -func getCachedMerkleTreeForTreasury(ctx context.Context, data code_data.Provider, address string) (*merkletree.MerkleTree, error) { - merkleTreeLock.Lock() - defer merkleTreeLock.Unlock() - - name, ok := treasuryPoolAddressToName[address] - if !ok { - treasuryPoolRecord, err := data.GetTreasuryPoolByAddress(ctx, address) - if err != nil { - return nil, err - } - name = treasuryPoolRecord.Name - treasuryPoolAddressToName[address] = name - } - - cached, ok := cachedMerkleTrees[name] - if !ok { - loaded, err := data.LoadExistingMerkleTree(ctx, name, true) - if err != nil { - return nil, err - } - - cached = &refreshingMerkleTree{ - tree: loaded, - lastRefreshedAt: time.Now(), - } - cachedMerkleTrees[name] = cached - } - - // Refresh the merkle tree periodically, instead of loading it every time. - // - // todo: configurable value (put an upper bound when it does become configurable) - if time.Since(cached.lastRefreshedAt) > time.Minute { - err := cached.tree.Refresh(ctx) - if err != nil { - return nil, err - } - - cached.lastRefreshedAt = time.Now() - } - - return cached.tree, nil -} diff --git a/pkg/code/async/commitment/metrics.go b/pkg/code/async/commitment/metrics.go deleted file mode 100644 index 8ff70f0f..00000000 --- a/pkg/code/async/commitment/metrics.go +++ /dev/null @@ -1,48 +0,0 @@ -package async_commitment - -import ( - "context" - "fmt" - "time" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/metrics" -) - -const ( - commitmentCountMetricName = "Commitment/%s_count" -) - -func (p *service) metricsGaugeWorker(ctx context.Context) error { - delay := time.Second - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(delay): - start := time.Now() - - for _, state := range []commitment.State{ - commitment.StateUnknown, - commitment.StatePayingDestination, - commitment.StateOpen, - commitment.StateClosing, - commitment.StateClosed, - } { - count, err := p.data.CountCommitmentsByState(ctx, state) - if err != nil { - continue - } - recordCommitmentCountMetric(ctx, state, count) - } - - delay = time.Second - time.Since(start) - } - } -} - -func recordCommitmentCountMetric(ctx context.Context, state commitment.State, count uint64) { - metricName := fmt.Sprintf(commitmentCountMetricName, state.String()) - metrics.RecordCount(ctx, metricName, count) -} diff --git a/pkg/code/async/commitment/service.go b/pkg/code/async/commitment/service.go deleted file mode 100644 index abb7d7bf..00000000 --- a/pkg/code/async/commitment/service.go +++ /dev/null @@ -1,55 +0,0 @@ -package async_commitment - -import ( - "context" - "time" - - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/async" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/commitment" -) - -type service struct { - log *logrus.Entry - data code_data.Provider -} - -func New(data code_data.Provider) async.Service { - return &service{ - log: logrus.StandardLogger().WithField("service", "commitment"), - data: data, - } -} - -func (p *service) Start(ctx context.Context, interval time.Duration) error { - - // Setup workers to watch for commitment state changes on the Solana side - for _, item := range []commitment.State{ - commitment.StateOpen, - commitment.StateClosed, - - // Note: All other states are handled externally, currently. - } { - go func(state commitment.State) { - err := p.worker(ctx, state, interval) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warnf("commitment processing loop terminated unexpectedly for state %d", state) - } - - }(item) - } - - go func() { - err := p.metricsGaugeWorker(ctx) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("treasury metrics gauge loop terminated unexpectedly") - } - }() - - select { - case <-ctx.Done(): - return ctx.Err() - } -} diff --git a/pkg/code/async/commitment/temporary_privacy.go b/pkg/code/async/commitment/temporary_privacy.go deleted file mode 100644 index 72f36078..00000000 --- a/pkg/code/async/commitment/temporary_privacy.go +++ /dev/null @@ -1,91 +0,0 @@ -package async_commitment - -import ( - "context" - "time" - - "github.com/code-payments/code-server/pkg/cache" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/merkletree" -) - -var ( - // todo: bump this significantly after clients implement upgrades - // todo: better configuration mechanism - privacyUpgradeTimeout = 15 * time.Minute - - privacyUpgradeTimeoutCache = cache.NewCache(1_000_000) -) - -// GetDeadlineToUpgradePrivacy figures out at what point in time the temporary -// private transfer to the commitment should be played out. If no deadline -// exists, then ErrNoPrivacyUpgradeDeadline is returned. -// -// todo: move this someplace more common? -func GetDeadlineToUpgradePrivacy(ctx context.Context, data code_data.Provider, commitmentRecord *commitment.Record) (*time.Time, error) { - if commitmentRecord.RepaymentDivertedTo != nil { - // There is no deadline because the privacy upgrade has already occurred - return nil, ErrNoPrivacyUpgradeDeadline - } - - if commitmentRecord.TreasuryRepaid { - // There is no deadline because the temporary private transfer has already - // been cashed in. - return nil, ErrNoPrivacyUpgradeDeadline - } - - actionRecord, err := data.GetActionById(ctx, commitmentRecord.Intent, commitmentRecord.ActionId) - if err != nil { - return nil, err - } - - timelockRecord, err := data.GetTimelockByVault(ctx, actionRecord.Source) - if err != nil { - return nil, err - } - - if !common.IsManagedByCode(ctx, timelockRecord) { - // The source account is no longer managed by Code. We need to cash this - // in yesterday. - t := time.Now().Add(-time.Hour) - return &t, nil - } - - cached, ok := privacyUpgradeTimeoutCache.Retrieve(commitmentRecord.Address) - if ok { - cloned := cached.(time.Time) - return &cloned, nil - } - - merkleTree, err := getCachedMerkleTreeForTreasury(ctx, data, commitmentRecord.Pool) - if err == merkletree.ErrMerkleTreeNotFound { - // Merkle tree doesn't exist, so we're likely operating on a new treasury - // pool - return nil, ErrNoPrivacyUpgradeDeadline - } else if err != nil { - return nil, err - } - - commitmentAccount, err := common.NewAccountFromPublicKeyString(commitmentRecord.Address) - if err != nil { - return nil, err - } - - leafNode, err := merkleTree.GetLeafNode(ctx, commitmentAccount.PublicKey().ToBytes()) - if err == merkletree.ErrLeafNotFound { - // The commitment hasn't even been observed by our local merkle tree, - // so keep pushing back the deadline. There's nothing we can do because - // the account can't even be opened without a proof, let alone have a - // proof for a client upgrade. - return nil, ErrNoPrivacyUpgradeDeadline - } else if err != nil { - return nil, err - } - - t := leafNode.CreatedAt.Add(privacyUpgradeTimeout) - cloned := t - privacyUpgradeTimeoutCache.Insert(commitmentRecord.Address, cloned, 1) - return &t, nil -} diff --git a/pkg/code/async/commitment/temporary_privacy_test.go b/pkg/code/async/commitment/temporary_privacy_test.go deleted file mode 100644 index 23ee31c1..00000000 --- a/pkg/code/async/commitment/temporary_privacy_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package async_commitment - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/commitment" -) - -func TestGetDeadlineToUpgradePrivacy_HappyPath(t *testing.T) { - env := setup(t) - - commitmentRecords := env.simulateCommitments(t, 2, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - commitmentRecord := commitmentRecords[0] - - // Commitment isn't in the merkle tree - _, err := GetDeadlineToUpgradePrivacy(env.ctx, env.data, commitmentRecord) - assert.Equal(t, ErrNoPrivacyUpgradeDeadline, err) - - env.simulateAddingLeaves(t, commitmentRecords) - - leafNode, err := env.merkleTree.GetLeafNodeByIndex(env.ctx, 0) - require.NoError(t, err) - - // Commitment is in the merkle tree and deadline is based on leaf creation timestamp - actual, err := GetDeadlineToUpgradePrivacy(env.ctx, env.data, commitmentRecord) - require.NoError(t, err) - assert.Equal(t, leafNode.CreatedAt.Add(privacyUpgradeTimeout), *actual) - - // Treasury is repaid, so privacy can't be upgraded - commitmentRecord.TreasuryRepaid = true - _, err = GetDeadlineToUpgradePrivacy(env.ctx, env.data, commitmentRecord) - assert.Equal(t, ErrNoPrivacyUpgradeDeadline, err) - commitmentRecord.TreasuryRepaid = false - - // Privacy is already upgraded - commitmentRecord.RepaymentDivertedTo = &commitmentRecords[1].VaultAddress - _, err = GetDeadlineToUpgradePrivacy(env.ctx, env.data, commitmentRecord) - assert.Equal(t, ErrNoPrivacyUpgradeDeadline, err) - commitmentRecord.RepaymentDivertedTo = nil - - // Source user account has been unlocked - env.simulateSourceAccountUnlocked(t, commitmentRecord) - actual, err = GetDeadlineToUpgradePrivacy(env.ctx, env.data, commitmentRecord) - require.NoError(t, err) - assert.True(t, actual.Before(time.Now())) -} diff --git a/pkg/code/async/commitment/testutil.go b/pkg/code/async/commitment/testutil.go deleted file mode 100644 index 4e143d0b..00000000 --- a/pkg/code/async/commitment/testutil.go +++ /dev/null @@ -1,289 +0,0 @@ -package async_commitment - -import ( - "context" - "encoding/hex" - "fmt" - "math/rand" - "testing" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/merkletree" - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana/cvm" - timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" - "github.com/code-payments/code-server/pkg/testutil" -) - -type testEnv struct { - ctx context.Context - data code_data.Provider - treasuryPool *treasury.Record - merkleTree *merkletree.MerkleTree - worker *service - subsidizer *common.Account -} - -func setup(t *testing.T) testEnv { - ctx := context.Background() - - db := code_data.NewTestDataProvider() - - privacyUpgradeCandidateSelectionTimeout = 0 - - subsidizer := testutil.SetupRandomSubsidizer(t, db) - - treasuryPoolAddress := testutil.NewRandomAccount(t) - treasuryPool := &treasury.Record{ - Vm: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - Name: "test-pool", - - Address: treasuryPoolAddress.PublicKey().ToBase58(), - Bump: 123, - - Vault: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - VaultBump: 100, - - Authority: subsidizer.PublicKey().ToBase58(), - - MerkleTreeLevels: 63, - - CurrentIndex: 1, - HistoryListSize: 5, - - SolanaBlock: 123, - - State: treasury.TreasuryPoolStateAvailable, - } - - merkleTree, err := db.InitializeNewMerkleTree( - ctx, - treasuryPool.Name, - treasuryPool.MerkleTreeLevels, - []merkletree.Seed{ - cvm.MerkleTreePrefix, - treasuryPoolAddress.PublicKey().ToBytes(), - }, - false, - ) - require.NoError(t, err) - - rootNode, err := merkleTree.GetCurrentRootNode(ctx) - require.NoError(t, err) - for i := 0; i < int(treasuryPool.HistoryListSize); i++ { - treasuryPool.HistoryList = append(treasuryPool.HistoryList, hex.EncodeToString(rootNode.Hash)) - } - require.NoError(t, db.SaveTreasuryPool(ctx, treasuryPool)) - - treasuryPoolAddressToName = make(map[string]string) - cachedMerkleTrees = make(map[string]*refreshingMerkleTree) - - return testEnv{ - ctx: ctx, - data: db, - treasuryPool: treasuryPool, - merkleTree: merkleTree, - worker: New(db).(*service), - subsidizer: subsidizer, - } -} - -func (e testEnv) simulateCommitment(t *testing.T, recentRoot string, state commitment.State) *commitment.Record { - vm, err := common.NewAccountFromPublicKeyString(e.treasuryPool.Vm) - require.NoError(t, err) - - commitmentRecord := &commitment.Record{ - Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - VaultAddress: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - Pool: e.treasuryPool.Address, - RecentRoot: recentRoot, - - Transcript: "transcript", - Destination: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Amount: kin.ToQuarks(1), - - Intent: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - ActionId: rand.Uint32(), - - Owner: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - State: state, - } - require.NoError(t, e.data.SaveCommitment(e.ctx, commitmentRecord)) - - owner := testutil.NewRandomAccount(t) - timelockAccounts, err := owner.GetTimelockAccounts(vm, common.KinMintAccount) - require.NoError(t, err) - - intentRecord := &intent.Record{ - IntentId: commitmentRecord.Intent, - IntentType: intent.SendPrivatePayment, - - InitiatorOwnerAccount: owner.PublicKey().ToBase58(), - - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - DestinationTokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Quantity: kin.ToQuarks(100), - - ExchangeCurrency: currency.KIN, - ExchangeRate: 1.0, - NativeAmount: 100, - UsdMarketValue: 1, - }, - - State: intent.StatePending, - } - require.NoError(t, e.data.SaveIntent(e.ctx, intentRecord)) - - actionRecord := &action.Record{ - Intent: commitmentRecord.Intent, - IntentType: intentRecord.IntentType, - - ActionId: commitmentRecord.ActionId, - ActionType: action.PrivateTransfer, - - Source: timelockAccounts.Vault.PublicKey().ToBase58(), - Destination: &commitmentRecord.Destination, - - State: action.StateUnknown, - } - require.NoError(t, e.data.PutAllActions(e.ctx, actionRecord)) - - fulfillmentRecord := &fulfillment.Record{ - Intent: commitmentRecord.Intent, - IntentType: intentRecord.IntentType, - - ActionId: commitmentRecord.ActionId, - ActionType: actionRecord.ActionType, - - FulfillmentType: fulfillment.TemporaryPrivacyTransferWithAuthority, - Data: []byte("data"), - Signature: pointer.String(fmt.Sprintf("sig%d", rand.Uint64())), - - Nonce: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - Blockhash: pointer.String("bh"), - - Source: actionRecord.Source, - Destination: &e.treasuryPool.Vault, - - State: fulfillment.StateUnknown, - } - require.NoError(t, e.data.PutAllFulfillments(e.ctx, fulfillmentRecord)) - - timelockRecord := timelockAccounts.ToDBRecord() - timelockRecord.VaultState = timelock_token_v1.StateLocked - timelockRecord.Block += 1 - require.NoError(t, e.data.SaveTimelock(e.ctx, timelockRecord)) - - return commitmentRecord -} - -func (e testEnv) simulateCommitments(t *testing.T, count int, recentRoot string, state commitment.State) []*commitment.Record { - var commitmentRecords []*commitment.Record - for i := 0; i < count; i++ { - commitmentRecords = append(commitmentRecords, e.simulateCommitment(t, recentRoot, state)) - } - return commitmentRecords -} - -func (e testEnv) simulateSourceAccountUnlocked(t *testing.T, commitmentRecord *commitment.Record) { - actionRecord, err := e.data.GetActionById(e.ctx, commitmentRecord.Intent, commitmentRecord.ActionId) - require.NoError(t, err) - - timelockRecord, err := e.data.GetTimelockByVault(e.ctx, actionRecord.Source) - require.NoError(t, err) - - timelockRecord.VaultState = timelock_token_v1.StateUnlocked - timelockRecord.Block += 1 - require.NoError(t, e.data.SaveTimelock(e.ctx, timelockRecord)) -} - -func (e testEnv) simulateAddingLeaves(t *testing.T, commitmentRecords []*commitment.Record) { - for _, commitmentRecord := range commitmentRecords { - require.True(t, commitmentRecord.State >= commitment.StateReadyToOpen) - - addressBytes, err := base58.Decode(commitmentRecord.Address) - require.NoError(t, err) - - require.NoError(t, e.merkleTree.AddLeaf(e.ctx, addressBytes)) - } - - require.NoError(t, e.merkleTree.Refresh(e.ctx)) - - rootNode, err := e.merkleTree.GetCurrentRootNode(e.ctx) - require.NoError(t, err) - - e.treasuryPool.CurrentIndex = (e.treasuryPool.CurrentIndex + 1) % e.treasuryPool.HistoryListSize - e.treasuryPool.HistoryList[e.treasuryPool.CurrentIndex] = hex.EncodeToString(rootNode.Hash) - e.treasuryPool.SolanaBlock += 1 - require.NoError(t, e.data.SaveTreasuryPool(e.ctx, e.treasuryPool)) -} - -func (e testEnv) simulateTemporaryPrivacyChequeCashed(t *testing.T, commitmentRecord *commitment.Record, newState fulfillment.State) { - fulfillmentRecords, err := e.data.GetAllFulfillmentsByTypeAndAction(e.ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, commitmentRecord.Intent, commitmentRecord.ActionId) - require.NoError(t, err) - require.Len(t, fulfillmentRecords, 1) - - fulfillmentRecord := fulfillmentRecords[0] - require.Equal(t, fulfillment.TemporaryPrivacyTransferWithAuthority, fulfillmentRecords[0].FulfillmentType) - require.Equal(t, fulfillment.StateUnknown, fulfillmentRecord.State) - fulfillmentRecord.State = newState - require.NoError(t, e.data.UpdateFulfillment(e.ctx, fulfillmentRecord)) -} - -func (e testEnv) simulatePermanentPrivacyChequeCashed(t *testing.T, commitmentRecord *commitment.Record, newState fulfillment.State) { - fulfillmentRecords, err := e.data.GetAllFulfillmentsByTypeAndAction(e.ctx, fulfillment.PermanentPrivacyTransferWithAuthority, commitmentRecord.Intent, commitmentRecord.ActionId) - require.NoError(t, err) - require.Len(t, fulfillmentRecords, 1) - - fulfillmentRecord := fulfillmentRecords[0] - require.Equal(t, fulfillment.PermanentPrivacyTransferWithAuthority, fulfillmentRecords[0].FulfillmentType) - require.Equal(t, fulfillment.StateUnknown, fulfillmentRecord.State) - fulfillmentRecord.State = newState - require.NoError(t, e.data.UpdateFulfillment(e.ctx, fulfillmentRecord)) -} - -func (e testEnv) simulateCommitmentBeingUpgraded(t *testing.T, upgradeFrom, upgradeTo *commitment.Record) { - require.Nil(t, upgradeFrom.RepaymentDivertedTo) - - upgradeFrom.RepaymentDivertedTo = &upgradeTo.VaultAddress - require.NoError(t, e.data.SaveCommitment(e.ctx, upgradeFrom)) - - fulfillmentRecords, err := e.data.GetAllFulfillmentsByTypeAndAction(e.ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, upgradeFrom.Intent, upgradeFrom.ActionId) - require.NoError(t, err) - require.Len(t, fulfillmentRecords, 1) - require.Equal(t, fulfillment.TemporaryPrivacyTransferWithAuthority, fulfillmentRecords[0].FulfillmentType) - - permanentPrivacyFulfillment := fulfillmentRecords[0].Clone() - permanentPrivacyFulfillment.Id = 0 - permanentPrivacyFulfillment.Signature = pointer.String(fmt.Sprintf("txn%d", rand.Uint64())) - permanentPrivacyFulfillment.FulfillmentType = fulfillment.PermanentPrivacyTransferWithAuthority - permanentPrivacyFulfillment.Destination = &e.treasuryPool.Vault - require.NoError(t, e.data.PutAllFulfillments(e.ctx, &permanentPrivacyFulfillment)) -} - -func (e testEnv) assertCommitmentState(t *testing.T, address string, expected commitment.State) { - commitmentRecord, err := e.data.GetCommitmentByAddress(e.ctx, address) - require.NoError(t, err) - assert.Equal(t, expected, commitmentRecord.State) -} - -func (e testEnv) assertCommitmentRepaymentStatus(t *testing.T, address string, expected bool) { - commitmentRecord, err := e.data.GetCommitmentByAddress(e.ctx, address) - require.NoError(t, err) - assert.Equal(t, expected, commitmentRecord.TreasuryRepaid) -} diff --git a/pkg/code/async/commitment/transaction.go b/pkg/code/async/commitment/transaction.go deleted file mode 100644 index 0d261e8e..00000000 --- a/pkg/code/async/commitment/transaction.go +++ /dev/null @@ -1,47 +0,0 @@ -package async_commitment - -import ( - "context" - "math" - - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" -) - -func (p *service) injectCloseCommitmentFulfillment(ctx context.Context, commitmentRecord *commitment.Record) error { - // Idempotency check to ensure we don't double up on fulfillments - _, err := p.data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.CloseCommitment, commitmentRecord.Intent, commitmentRecord.ActionId) - if err == nil { - return nil - } else if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return err - } - - intentRecord, err := p.data.GetIntent(ctx, commitmentRecord.Intent) - if err != nil { - return err - } - - // Transaction is created on demand at time of scheduling - fulfillmentRecord := &fulfillment.Record{ - Intent: intentRecord.IntentId, - IntentType: intentRecord.IntentType, - - ActionId: commitmentRecord.ActionId, - ActionType: action.PrivateTransfer, - - FulfillmentType: fulfillment.CloseCommitment, - - Source: commitmentRecord.VaultAddress, - - IntentOrderingIndex: uint64(math.MaxInt64), - ActionOrderingIndex: 0, - FulfillmentOrderingIndex: 0, - - DisableActiveScheduling: false, - - State: fulfillment.StateUnknown, - } - return p.data.PutAllFulfillments(ctx, fulfillmentRecord) -} diff --git a/pkg/code/async/commitment/util.go b/pkg/code/async/commitment/util.go deleted file mode 100644 index 3d68b763..00000000 --- a/pkg/code/async/commitment/util.go +++ /dev/null @@ -1,61 +0,0 @@ -package async_commitment - -import ( - "context" - "errors" - - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/commitment" -) - -// Every other state is currently managed after successful fulfillment submission - -func markCommitmentAsClosing(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return err - } - - if commitmentRecord.State == commitment.StateClosing { - return nil - } - - if commitmentRecord.State != commitment.StateOpen { - return errors.New("commitment in invalid state") - } - - commitmentRecord.State = commitment.StateClosing - return data.SaveCommitment(ctx, commitmentRecord) -} - -func markCommitmentReadyForGC(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return err - } - - if commitmentRecord.State == commitment.StateReadyToRemoveFromMerkleTree { - return nil - } - - if commitmentRecord.State != commitment.StateReadyToOpen && commitmentRecord.State != commitment.StateClosed { - return errors.New("commitment in invalid state") - } - - commitmentRecord.State = commitment.StateReadyToRemoveFromMerkleTree - return data.SaveCommitment(ctx, commitmentRecord) -} - -func markTreasuryAsRepaid(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return err - } - - if commitmentRecord.TreasuryRepaid { - return nil - } - - commitmentRecord.TreasuryRepaid = true - return data.SaveCommitment(ctx, commitmentRecord) -} diff --git a/pkg/code/async/commitment/worker.go b/pkg/code/async/commitment/worker.go deleted file mode 100644 index f6d72ffc..00000000 --- a/pkg/code/async/commitment/worker.go +++ /dev/null @@ -1,253 +0,0 @@ -package async_commitment - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/mr-tron/base58" - "github.com/newrelic/go-agent/v3/newrelic" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/merkletree" - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/retry" -) - -// -// Disclaimer: -// State transition logic is tightly coupled to assumptions of logic for how we -// move between states and how we pick a commitment account to divert repayments -// to. This simplifies the local logic, but does mean we need to be careful making -// updates. -// - -const ( - maxRecordBatchSize = 100 -) - -var ( - // Timeout when we know SubmitIntent won't select a commitment as a candidate - // for a privacy upgrade. Don't lower this any further. - // - // todo: configurable - privacyUpgradeCandidateSelectionTimeout = 10 * time.Minute -) - -var ( - ErrNoPrivacyUpgradeDeadline = errors.New("no privacy upgrade deadline for commitment") -) - -func (p *service) worker(serviceCtx context.Context, state commitment.State, interval time.Duration) error { - delay := interval - var cursor query.Cursor - - err := retry.Loop( - func() (err error) { - time.Sleep(delay) - - nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) - m := nr.StartTransaction("async__commitment_service__handle_" + state.String()) - defer m.End() - tracedCtx := newrelic.NewContext(serviceCtx, m) - - // Get a batch of records in similar state - items, err := p.data.GetAllCommitmentsByState( - tracedCtx, - state, - query.WithCursor(cursor), - query.WithDirection(query.Ascending), - query.WithLimit(maxRecordBatchSize), - ) - if err != nil && err != treasury.ErrTreasuryPoolNotFound { - cursor = query.EmptyCursor - return err - } - - // Process the batch of commitments in parallel - var wg sync.WaitGroup - for _, item := range items { - wg.Add(1) - go func(record *commitment.Record) { - defer wg.Done() - - err := p.handle(tracedCtx, record) - if err != nil { - m.NoticeError(err) - } - }(item) - } - wg.Wait() - - // Update cursor to point to the next set of pool - if len(items) > 0 { - cursor = query.ToCursor(items[len(items)-1].Id) - } else { - cursor = query.EmptyCursor - } - - return nil - }, - retry.NonRetriableErrors(context.Canceled), - ) - - return err -} - -// todo: needs to lock a distributed lock -func (p *service) handle(ctx context.Context, record *commitment.Record) error { - switch record.State { - case commitment.StateOpen: - return p.handleOpen(ctx, record) - case commitment.StateClosed: - return p.handleClosed(ctx, record) - default: - return nil - } -} - -func (p *service) handleOpen(ctx context.Context, record *commitment.Record) error { - err := p.maybeMarkTreasuryAsRepaid(ctx, record) - if err != nil { - return err - } - - shouldClose, err := p.shouldCloseCommitment(ctx, record) - if err != nil { - return err - } - - if shouldClose { - err = p.injectCloseCommitmentFulfillment(ctx, record) - if err != nil { - return err - } - - return markCommitmentAsClosing(ctx, p.data, record.Intent, record.ActionId) - } - - return nil -} - -func (p *service) handleClosed(ctx context.Context, record *commitment.Record) error { - err := p.maybeMarkTreasuryAsRepaid(ctx, record) - if err != nil { - return err - } - - return p.maybeMarkCommitmentForGC(ctx, record) -} - -func (p *service) shouldCloseCommitment(ctx context.Context, commitmentRecord *commitment.Record) (bool, error) { - if commitmentRecord.State != commitment.StateOpen { - return false, nil - } - - // - // Part 1: Ensure we either upgraded the temporary private transfer or confirmed it - // - - if commitmentRecord.RepaymentDivertedTo == nil && !commitmentRecord.TreasuryRepaid { - return false, nil - } - - // - // Part 2: Ensure we won't select this commitment vault for any new upgrades - // - // Note: As a result, this is directly tied to what we do in SubmitIntent to - // select a commitment account to divert funds to. - // - - merkleTree, err := getCachedMerkleTreeForTreasury(ctx, p.data, commitmentRecord.Pool) - if err != nil { - return false, err - } - - commitmentAddressBytes, err := base58.Decode(commitmentRecord.Address) - if err != nil { - return false, err - } - - leafNode, err := merkleTree.GetLeafNode(ctx, commitmentAddressBytes) - if err == merkletree.ErrLeafNotFound { - return false, nil - } else if err != nil { - return false, err - } - - nexLeafNode, err := merkleTree.GetLeafNodeByIndex(ctx, leafNode.Index+1) - if err == merkletree.ErrLeafNotFound { - return false, nil - } else if err != nil { - return false, err - } - - if time.Since(nexLeafNode.CreatedAt) < privacyUpgradeCandidateSelectionTimeout { - // There's a newer commitment, so the current one isn't a candidate to divert - // to anymore. Wait until it reaches a certain age to avoid any chance of a - // race condition with using cached merkle trees in SubmitIntent. This time check - // is exactly why we query for the next leaf versus say the latest. - // - // todo: Do something smarter when we have distributed locks. - return false, nil - } - - // - // Part 3: Ensure all upgraded private transfers going to this commitment have been confirmed - // - - numPendingDivertedRepayments, err := p.data.CountPendingCommitmentRepaymentsDivertedToCommitment(ctx, commitmentRecord.Address) - if err != nil { - return false, err - } - - // All diverted repayments need to be confirmed before closing the commitment - if numPendingDivertedRepayments > 0 { - return false, nil - } - - return true, nil -} - -func (p *service) maybeMarkCommitmentForGC(ctx context.Context, commitmentRecord *commitment.Record) error { - // Can't GC until we know the treasury has been repaid - if !commitmentRecord.TreasuryRepaid { - return nil - } - - // The commitment is closed because it has reached a terminal state and we - // are done with it. All temporary and permaenent cheques that are targets - // for this commitment should be cashed. Proceed to GC. - if commitmentRecord.State == commitment.StateClosed { - return markCommitmentReadyForGC(ctx, p.data, commitmentRecord.Intent, commitmentRecord.ActionId) - } - - return nil -} - -func (p *service) maybeMarkTreasuryAsRepaid(ctx context.Context, commitmentRecord *commitment.Record) error { - if commitmentRecord.TreasuryRepaid { - return nil - } - - fulfillmentTypeToCheck := fulfillment.TemporaryPrivacyTransferWithAuthority - if commitmentRecord.RepaymentDivertedTo != nil { - fulfillmentTypeToCheck = fulfillment.PermanentPrivacyTransferWithAuthority - } - - fulfillmentRecords, err := p.data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillmentTypeToCheck, commitmentRecord.Intent, commitmentRecord.ActionId) - if err != nil { - return err - } - - // The cheque hasn't been cashed, so we cannot mark the treasury as being repaid - if fulfillmentRecords[0].State != fulfillment.StateConfirmed { - return nil - } - - return nil -} diff --git a/pkg/code/async/commitment/worker_test.go b/pkg/code/async/commitment/worker_test.go deleted file mode 100644 index 20caa17f..00000000 --- a/pkg/code/async/commitment/worker_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package async_commitment - -// todo: add tests once commitment flows are finalized diff --git a/pkg/code/async/geyser/backup.go b/pkg/code/async/geyser/backup.go index eb159993..e036ba57 100644 --- a/pkg/code/async/geyser/backup.go +++ b/pkg/code/async/geyser/backup.go @@ -7,7 +7,6 @@ import ( "github.com/newrelic/go-agent/v3/newrelic" - "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/metrics" @@ -133,51 +132,3 @@ func (p *service) backupExternalDepositWorker(serviceCtx context.Context, interv } } } - -func (p *service) backupMessagingWorker(serviceCtx context.Context, interval time.Duration) error { - log := p.log.WithField("method", "backupMessagingWorker") - log.Debug("worker started") - - p.metricStatusLock.Lock() - p.backupMessagingWorkerStatus = true - p.metricStatusLock.Unlock() - defer func() { - p.metricStatusLock.Lock() - p.backupMessagingWorkerStatus = false - p.metricStatusLock.Unlock() - - log.Debug("worker stopped") - }() - - delay := 0 * time.Second // Initially no delay, so we can run right after a deploy - - messagingFeeCollector, err := common.NewAccountFromPublicKeyString(p.conf.messagingFeeCollectorPublicKey.Get(serviceCtx)) - if err != nil { - return err - } - - var checkpoint *string - for { - select { - case <-time.After(delay): - start := time.Now() - - func() { - nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) - m := nr.StartTransaction("async__geyser_consumer_service__backup_messaging_worker") - defer m.End() - tracedCtx := newrelic.NewContext(serviceCtx, m) - - checkpoint, err = fixMissingBlockchainMessages(tracedCtx, p.data, p.pusher, messagingFeeCollector, checkpoint) - if err != nil { - m.NoticeError(err) - log.WithError(err).Warn("failure fixing missing messages") - } - }() - - delay = interval - time.Since(start) - case <-serviceCtx.Done(): - return serviceCtx.Err() - } - } -} diff --git a/pkg/code/async/geyser/external_deposit.go b/pkg/code/async/geyser/external_deposit.go index 969392ff..e7fe38b2 100644 --- a/pkg/code/async/geyser/external_deposit.go +++ b/pkg/code/async/geyser/external_deposit.go @@ -15,19 +15,15 @@ import ( transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" "github.com/code-payments/code-server/pkg/cache" - chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/balance" - "github.com/code-payments/code-server/pkg/code/data/chat" "github.com/code-payments/code-server/pkg/code/data/onramp" - "github.com/code-payments/code-server/pkg/code/push" "github.com/code-payments/code-server/pkg/code/thirdparty" currency_lib "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/grpc/client" - push_lib "github.com/code-payments/code-server/pkg/push" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/usdc" ) @@ -42,7 +38,7 @@ var ( syncedDepositCache = cache.NewCache(1_000_000) ) -func fixMissingExternalDeposits(ctx context.Context, conf *conf, data code_data.Provider, pusher push_lib.Provider, vault *common.Account) error { +func fixMissingExternalDeposits(ctx context.Context, conf *conf, data code_data.Provider, vault *common.Account) error { signatures, err := findPotentialExternalDeposits(ctx, data, vault) if err != nil { return errors.Wrap(err, "error finding potential external deposits") @@ -50,7 +46,7 @@ func fixMissingExternalDeposits(ctx context.Context, conf *conf, data code_data. var anyError error for _, signature := range signatures { - err := processPotentialExternalDeposit(ctx, conf, data, pusher, signature, vault) + err := processPotentialExternalDeposit(ctx, conf, data, signature, vault) if err != nil { anyError = errors.Wrap(err, "error processing signature for external deposit") } @@ -114,7 +110,7 @@ func findPotentialExternalDeposits(ctx context.Context, data code_data.Provider, return nil, errors.New("not implemented") } -func processPotentialExternalDeposit(ctx context.Context, conf *conf, data code_data.Provider, pusher push_lib.Provider, signature string, tokenAccount *common.Account) error { +func processPotentialExternalDeposit(ctx context.Context, conf *conf, data code_data.Provider, signature string, tokenAccount *common.Account) error { /* // Avoid reprocessing deposits we've recently seen and processed. Particularly, // the backup process will likely be triggered in frequent bursts, so this is @@ -701,7 +697,6 @@ func delayedUsdcDepositProcessing( ctx context.Context, conf *conf, data code_data.Provider, - pusher push_lib.Provider, ownerAccount *common.Account, tokenAccount *common.Account, signature string, @@ -748,29 +743,6 @@ func delayedUsdcDepositProcessing( return } } - - chatMessage, err := chat_util.ToUsdcDepositedMessage(signature, blockTime) - if err != nil { - return - } - - canPush, err := chat_util.SendKinPurchasesMessage(ctx, data, ownerAccount, chatMessage) - switch err { - case nil: - if canPush { - push.SendChatMessagePushNotification( - ctx, - data, - pusher, - chat_util.KinPurchasesName, - ownerAccount, - chatMessage, - ) - } - case chat.ErrMessageAlreadyExists: - default: - return - } } // Optimistically tries to cache a balance for an external account not managed diff --git a/pkg/code/async/geyser/handler.go b/pkg/code/async/geyser/handler.go index 2f00b35f..2188dd93 100644 --- a/pkg/code/async/geyser/handler.go +++ b/pkg/code/async/geyser/handler.go @@ -11,7 +11,6 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - push_lib "github.com/code-payments/code-server/pkg/push" "github.com/code-payments/code-server/pkg/solana/token" ) @@ -28,16 +27,14 @@ type ProgramAccountUpdateHandler interface { } type TokenProgramAccountHandler struct { - conf *conf - data code_data.Provider - pusher push_lib.Provider + conf *conf + data code_data.Provider } -func NewTokenProgramAccountHandler(conf *conf, data code_data.Provider, pusher push_lib.Provider) ProgramAccountUpdateHandler { +func NewTokenProgramAccountHandler(conf *conf, data code_data.Provider) ProgramAccountUpdateHandler { return &TokenProgramAccountHandler{ - conf: conf, - data: data, - pusher: pusher, + conf: conf, + data: data, } } @@ -80,18 +77,6 @@ func (h *TokenProgramAccountHandler) Handle(ctx context.Context, update *geyserp return errors.Wrap(err, "invalid mint account") } - // The token account is the messaging fee collector, so process the update as - // a blockchain message. - if tokenAccount.PublicKey().ToBase58() == h.conf.messagingFeeCollectorPublicKey.Get(ctx) { - return processPotentialBlockchainMessage( - ctx, - h.data, - h.pusher, - tokenAccount, - *update.TxSignature, - ) - } - // Account is empty, and all we care about are external deposits at this point, // so filter it out if unmarshalled.Amount == 0 { @@ -100,20 +85,15 @@ func (h *TokenProgramAccountHandler) Handle(ctx context.Context, update *geyserp switch mintAccount.PublicKey().ToBase58() { - case common.KinMintAccount.PublicKey().ToBase58(): + case common.CoreMintAccount.PublicKey().ToBase58(): // Not a program vault account, so filter it out. It cannot be a Timelock // account. if !bytes.Equal(tokenAccount.PublicKey().ToBytes(), ownerAccount.PublicKey().ToBytes()) { return nil } - isCodeTimelockAccount, err := testForKnownCodeTimelockAccount(ctx, h.data, tokenAccount) - if err != nil { - return errors.Wrap(err, "error testing for known account") - } else if !isCodeTimelockAccount { - // Not an account we track, so skip the update - return nil - } + // todo: Need to implement VM deposit flow + return nil case common.UsdcMintAccount.PublicKey().ToBase58(): ata, err := ownerAccount.ToAssociatedTokenAccount(common.UsdcMintAccount) @@ -135,17 +115,17 @@ func (h *TokenProgramAccountHandler) Handle(ctx context.Context, update *geyserp } default: - // Not a Kin or USDC account, so filter it out + // Not a Core Mint or USDC account, so filter it out return nil } // We've determined this token account is one that we care about. Process // the update as an external deposit. - return processPotentialExternalDeposit(ctx, h.conf, h.data, h.pusher, *update.TxSignature, tokenAccount) + return processPotentialExternalDeposit(ctx, h.conf, h.data, *update.TxSignature, tokenAccount) } -func initializeProgramAccountUpdateHandlers(conf *conf, data code_data.Provider, pusher push_lib.Provider) map[string]ProgramAccountUpdateHandler { +func initializeProgramAccountUpdateHandlers(conf *conf, data code_data.Provider) map[string]ProgramAccountUpdateHandler { return map[string]ProgramAccountUpdateHandler{ - base58.Encode(token.ProgramKey): NewTokenProgramAccountHandler(conf, data, pusher), + base58.Encode(token.ProgramKey): NewTokenProgramAccountHandler(conf, data), } } diff --git a/pkg/code/async/geyser/handler_test.go b/pkg/code/async/geyser/handler_test.go index 935680ca..ef1153fd 100644 --- a/pkg/code/async/geyser/handler_test.go +++ b/pkg/code/async/geyser/handler_test.go @@ -25,6 +25,6 @@ func setup(t *testing.T) *testEnv { data := code_data.NewTestDataProvider() return &testEnv{ data: data, - handlers: initializeProgramAccountUpdateHandlers(&conf{}, data, nil), + handlers: initializeProgramAccountUpdateHandlers(&conf{}, data), } } diff --git a/pkg/code/async/geyser/messenger.go b/pkg/code/async/geyser/messenger.go deleted file mode 100644 index 5c94153c..00000000 --- a/pkg/code/async/geyser/messenger.go +++ /dev/null @@ -1,278 +0,0 @@ -package async_geyser - -import ( - "context" - "slices" - "time" - - "github.com/mr-tron/base58" - "github.com/pkg/errors" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - - "github.com/code-payments/code-server/pkg/cache" - chat_util "github.com/code-payments/code-server/pkg/code/chat" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/push" - "github.com/code-payments/code-server/pkg/code/thirdparty" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/kin" - push_lib "github.com/code-payments/code-server/pkg/push" - "github.com/code-payments/code-server/pkg/retry" - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/memo" - "github.com/code-payments/code-server/pkg/solana/token" -) - -var ( - syncedMessageCache = cache.NewCache(1_000_000) -) - -// This assumes a specific structure for the transaction: -// - Legacy Transaction Format -// - Instruction[0] = Memo containing encoded message -// - Instruction[1] = Fee payment to Code -// - Message fee payer = Account verified against domain -// -// Any deviation from the expectation, and the message (or parts of it) won't be processed. -func processPotentialBlockchainMessage(ctx context.Context, data code_data.Provider, pusher push_lib.Provider, feeCollector *common.Account, signature string) error { - decodedSignature, err := base58.Decode(signature) - if err != nil { - return errors.Wrap(err, "invalid signature") - } - var typedSignature solana.Signature - copy(typedSignature[:], decodedSignature) - - if _, ok := syncedMessageCache.Retrieve(signature); ok { - return nil - } - - err = func() error { - // Only wait for a confirmed transaction, so we can optimize for speed of - // message delivery. Obviously, we wouldn't want to do anything with the - // user payment instruction, which should be handled by the external deposit - // logic that waits for finalization. - var txn *solana.ConfirmedTransaction - var err error - _, err = retry.Retry( - func() error { - txn, err = data.GetBlockchainTransaction(ctx, signature, solana.CommitmentConfirmed) - return err - }, - waitForConfirmationRetryStrategies..., - ) - if err != nil { - return errors.Wrap(err, "error getting transaction") - } - - if txn.Err != nil || txn.Meta.Err != nil { - return nil - } - - if len(txn.Transaction.Message.Instructions) != 2 { - return nil - } - - // Contains encoded message - memoIxn, err := memo.DecompileMemo(txn.Transaction.Message, 0) - if err != nil { - return nil - } - - // Links to Code, which trivially enables a Geyser listener - payCodeFeeIxn, err := token.DecompileTransfer(txn.Transaction.Message, 1) - if err != nil { - return nil - } else if base58.Encode(payCodeFeeIxn.Destination) != feeCollector.PublicKey().ToBase58() { - return nil - } - - feePayer, err := common.NewAccountFromPublicKeyBytes(payCodeFeeIxn.Owner) - if err != nil { - return nil - } - - // Process the transaction if the minimum fee was paid - // - // todo: configurable - // todo: set the real value when known - if payCodeFeeIxn.Amount >= kin.ToQuarks(1) { - // - // Attempt to parse the blockchain message from the memo payload - // - - if len(memoIxn.Data) == 0 { - return nil - } - - blockchainMessage, err := thirdparty.DecodeNaclBoxBlockchainMessage(memoIxn.Data) - if err != nil { - return nil - } - - // - // Verify domain name ownership - // - - asciiBaseDomain, err := thirdparty.GetAsciiBaseDomain(blockchainMessage.SenderDomain) - if err != nil { - return nil - } - - ownsDomain, err := thirdparty.VerifyDomainNameOwnership(ctx, feePayer, blockchainMessage.SenderDomain) - if err != nil || !ownsDomain { - // Third party being down should not affect progress of other - // critical systems like backup, so give up on this message - // and return nil. - return nil - } - - // - // Check the receiving account has a relationship with the verified domain - // - - accountInfoRecord, err := data.GetAccountInfoByAuthorityAddress(ctx, blockchainMessage.ReceiverAccount.PublicKey().ToBase58()) - if err == account.ErrAccountInfoNotFound { - return nil - } else if err != nil { - return errors.Wrap(err, "error getting account info") - } - - if accountInfoRecord.AccountType != commonpb.AccountType_RELATIONSHIP { - return nil - } else if *accountInfoRecord.RelationshipTo != asciiBaseDomain { - return nil - } - - // - // Surface the message with a push and storing it into chat history - // - - recipientOwner, err := common.NewAccountFromPublicKeyString(accountInfoRecord.OwnerAccount) - if err != nil { - return errors.Wrap(err, "invalid owner account") - } - - blockTime := time.Now() - if txn.BlockTime != nil { - blockTime = *txn.BlockTime - } - chatMessage, err := chat_util.ToBlockchainMessage(signature, feePayer, blockchainMessage, blockTime) - if err != nil { - return errors.Wrap(err, "error creating proto message") - } - - canPush, err := chat_util.SendChatMessage( - ctx, - data, - asciiBaseDomain, - chat.ChatTypeExternalApp, - true, - recipientOwner, - chatMessage, - false, - ) - if err != nil && err != chat.ErrMessageAlreadyExists { - return errors.Wrap(err, "error persisting chat message") - } - - if canPush { - // Best-effort send a push - push.SendChatMessagePushNotification( - ctx, - data, - pusher, - asciiBaseDomain, - recipientOwner, - chatMessage, - ) - } - } - - return nil - }() - - if err == nil { - syncedMessageCache.Insert(signature, true, 1) - } - return err -} - -// todo: strategy might need to change at a very large scale -// todo: will likely need to add some parallelism to message processing once we have a decent scale -func fixMissingBlockchainMessages(ctx context.Context, data code_data.Provider, pusher push_lib.Provider, feeCollector *common.Account, checkpoint *string) (*string, error) { - signatures, err := findPotentialBlockchainMessages(ctx, data, feeCollector, checkpoint) - if err != nil { - return checkpoint, errors.Wrap(err, "error finding potential messages") - } - - // Oldest signatures first - slices.Reverse(signatures) - - var anyError error - for _, signature := range signatures { - err := processPotentialBlockchainMessage(ctx, data, pusher, feeCollector, signature) - if err != nil { - anyError = errors.Wrap(err, "error processing signature for message") - } - - if anyError == nil { - checkpoint = &signature - } - } - - return checkpoint, anyError -} - -func findPotentialBlockchainMessages(ctx context.Context, data code_data.Provider, feeCollector *common.Account, untilSignature *string) ([]string, error) { - var res []string - var cursor []byte - var totalTransactionsFound int - for { - history, err := data.GetBlockchainHistory( - ctx, - feeCollector.PublicKey().ToBase58(), - solana.CommitmentFinalized, - query.WithLimit(1_000), // Max supported - query.WithCursor(cursor), - ) - if err != nil { - return nil, errors.Wrap(err, "error getting signatures for address") - } - - if len(history) == 0 { - return res, nil - } - - for _, historyItem := range history { - signature := base58.Encode(historyItem.Signature[:]) - - if untilSignature != nil && signature == *untilSignature { - return res, nil - } - - // Transaction has an error, so skip it - if historyItem.Err != nil { - continue - } - - res = append(res, signature) - - // Bound total results - if len(res) >= 1_000 { - return res, nil - } - } - - // Bound total history to look for in the past - totalTransactionsFound += len(history) - if totalTransactionsFound >= 10_000 { - return res, nil - } - - cursor = query.Cursor(history[len(history)-1].Signature[:]) - } -} diff --git a/pkg/code/async/geyser/service.go b/pkg/code/async/geyser/service.go index bccb2a9e..a4abaeb1 100644 --- a/pkg/code/async/geyser/service.go +++ b/pkg/code/async/geyser/service.go @@ -10,7 +10,6 @@ import ( "github.com/code-payments/code-server/pkg/code/async" geyserpb "github.com/code-payments/code-server/pkg/code/async/geyser/api/gen" code_data "github.com/code-payments/code-server/pkg/code/data" - push_lib "github.com/code-payments/code-server/pkg/push" ) type eventWorkerMetrics struct { @@ -19,10 +18,9 @@ type eventWorkerMetrics struct { } type service struct { - log *logrus.Entry - data code_data.Provider - pusher push_lib.Provider - conf *conf + log *logrus.Entry + data code_data.Provider + conf *conf programUpdatesChan chan *geyserpb.AccountUpdate programUpdateHandlers map[string]ProgramAccountUpdateHandler @@ -43,15 +41,14 @@ type service struct { backupMessagingWorkerStatus bool } -func New(data code_data.Provider, pusher push_lib.Provider, configProvider ConfigProvider) async.Service { +func New(data code_data.Provider, configProvider ConfigProvider) async.Service { conf := configProvider() return &service{ log: logrus.StandardLogger().WithField("service", "geyser_consumer"), data: data, - pusher: pusher, conf: configProvider(), programUpdatesChan: make(chan *geyserpb.AccountUpdate, conf.programUpdateQueueSize.Get(context.Background())), - programUpdateHandlers: initializeProgramAccountUpdateHandlers(conf, data, pusher), + programUpdateHandlers: initializeProgramAccountUpdateHandlers(conf, data), programUpdateWorkerMetrics: make(map[int]*eventWorkerMetrics), } } diff --git a/pkg/code/async/geyser/timelock.go b/pkg/code/async/geyser/timelock.go index 69dad069..013863b6 100644 --- a/pkg/code/async/geyser/timelock.go +++ b/pkg/code/async/geyser/timelock.go @@ -45,7 +45,7 @@ func getTimelockUnlockState(ctx context.Context, data code_data.Provider, timelo return nil, 0, err } - timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) if err != nil { return nil, 0, err } diff --git a/pkg/code/async/sequencer/action_handler.go b/pkg/code/async/sequencer/action_handler.go index f23b25d1..3f78d433 100644 --- a/pkg/code/async/sequencer/action_handler.go +++ b/pkg/code/async/sequencer/action_handler.go @@ -123,70 +123,6 @@ func (h *NoPrivacyWithdrawActionHandler) OnFulfillmentStateChange(ctx context.Co return nil } -type PrivateTransferActionHandler struct { - data code_data.Provider -} - -func NewPrivateTransferActionHandler(data code_data.Provider) ActionHandler { - return &PrivateTransferActionHandler{ - data: data, - } -} - -// There's many fulfillments for a private transfer action, so we define success -// and failure purely based on our ability to move funds to/from the user accounts. -func (h *PrivateTransferActionHandler) OnFulfillmentStateChange(ctx context.Context, fulfillmentRecord *fulfillment.Record, newState fulfillment.State) error { - switch fulfillmentRecord.FulfillmentType { - case fulfillment.TemporaryPrivacyTransferWithAuthority, fulfillment.PermanentPrivacyTransferWithAuthority: - if newState == fulfillment.StateConfirmed { - return markActionConfirmed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - } - - if newState == fulfillment.StateFailed { - return markActionFailed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - } - case fulfillment.TransferWithCommitment: - // No handling of confirmed state, since the other end of the split transfer - // can't be run until the advance from the treasury is made to the destination. - if newState == fulfillment.StateFailed { - return markActionFailed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - } - case fulfillment.CloseCommitment: - // Don't care about commitment states. These are managed elsewhere. - return nil - default: - return errors.New("unexpected fulfillment type") - } - - return nil -} - -type SaveRecentRootActionHandler struct { - data code_data.Provider -} - -func NewSaveRecentRootActionHandler(data code_data.Provider) ActionHandler { - return &SaveRecentRootActionHandler{ - data: data, - } -} - -func (h *SaveRecentRootActionHandler) OnFulfillmentStateChange(ctx context.Context, fulfillmentRecord *fulfillment.Record, newState fulfillment.State) error { - if fulfillmentRecord.FulfillmentType != fulfillment.SaveRecentRoot { - return errors.New("unexpected fulfillment type") - } - - if newState == fulfillment.StateConfirmed { - return markActionConfirmed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - } - - if newState == fulfillment.StateFailed { - return markActionFailed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - } - - return nil -} - func validateActionState(record *action.Record, states ...action.State) error { for _, validState := range states { if record.State == validState { @@ -259,7 +195,5 @@ func getActionHandlers(data code_data.Provider) map[action.Type]ActionHandler { handlersByType[action.CloseEmptyAccount] = NewCloseEmptyAccountActionHandler(data) handlersByType[action.NoPrivacyTransfer] = NewNoPrivacyTransferActionHandler(data) handlersByType[action.NoPrivacyWithdraw] = NewNoPrivacyWithdrawActionHandler(data) - handlersByType[action.PrivateTransfer] = NewPrivateTransferActionHandler(data) - handlersByType[action.SaveRecentRoot] = NewSaveRecentRootActionHandler(data) return handlersByType } diff --git a/pkg/code/async/sequencer/commitment.go b/pkg/code/async/sequencer/commitment.go deleted file mode 100644 index d2cabc9d..00000000 --- a/pkg/code/async/sequencer/commitment.go +++ /dev/null @@ -1,79 +0,0 @@ -package async_sequencer - -import ( - "context" - "errors" - - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" -) - -// todo: commitment state is highly correlated with fulfillment submission, so we manage -// it here, at least for now - -func markCommitmentPayingDestination(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return err - } - - if commitmentRecord.State == commitment.StatePayingDestination { - return nil - } - - if commitmentRecord.State != commitment.StateUnknown { - return errors.New("commitment in invalid state") - } - - commitmentRecord.State = commitment.StatePayingDestination - return data.SaveCommitment(ctx, commitmentRecord) -} - -func markCommitmentOpen(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return err - } - - if commitmentRecord.State == commitment.StateOpen { - return nil - } - - if commitmentRecord.State != commitment.StatePayingDestination { - return errors.New("commitment in invalid state") - } - - // todo: We lose out on some scheduling optimizations now that commitments - // are opened immediately. There's now active polling during the entire - // temporary privacy deadline window. - fulfillmentRecords, err := data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, intentId, actionId) - if err != nil { - return err - } - err = markFulfillmentAsActivelyScheduled(ctx, data, fulfillmentRecords[0]) - if err != nil { - return err - } - - commitmentRecord.State = commitment.StateOpen - return data.SaveCommitment(ctx, commitmentRecord) -} - -func markCommitmentClosed(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) error { - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return err - } - - if commitmentRecord.State == commitment.StateClosed { - return nil - } - - if commitmentRecord.State != commitment.StateClosing { - return errors.New("commitment in invalid state") - } - - commitmentRecord.State = commitment.StateClosed - return data.SaveCommitment(ctx, commitmentRecord) -} diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index d57595a7..d369b734 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -2,38 +2,25 @@ package async_sequencer import ( "context" - "encoding/hex" "errors" - "sync" - "time" "github.com/mr-tron/base58" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" indexerpb "github.com/code-payments/code-vm-indexer/generated/indexer/v1" - commitment_worker "github.com/code-payments/code-server/pkg/code/async/commitment" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/cvm/storage" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/code-payments/code-server/pkg/code/data/treasury" transaction_util "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/cvm" "github.com/code-payments/code-server/pkg/solana/token" ) -var ( - // Global treasury pool lock - // - // todo: Use a distributed lock - treasuryPoolLock sync.Mutex -) - type FulfillmentHandler interface { // CanSubmitToBlockchain determines whether the given fulfillment can be // scheduled for submission to the blockchain. @@ -150,7 +137,7 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransact return nil, err } - timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) if err != nil { return nil, err } @@ -256,7 +243,7 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) OnSuccess(ctx context return errors.New("invalid fulfillment type") } - return savePaymentRecord(ctx, h.data, fulfillmentRecord, txnRecord) + return nil } func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { @@ -563,11 +550,6 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) OnSuccess(ctx context.Context, ful return errors.New("invalid fulfillment type") } - err := savePaymentRecord(ctx, h.data, fulfillmentRecord, txnRecord) - if err != nil { - return err - } - return onVirtualAccountDeleted(ctx, h.data, fulfillmentRecord.Source) } @@ -588,163 +570,93 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) IsRevoked(ctx context.Context, ful return false, false, nil } -type TemporaryPrivacyTransferWithAuthorityFulfillmentHandler struct { - conf *conf +type CloseEmptyTimelockAccountFulfillmentHandler struct { data code_data.Provider vmIndexerClient indexerpb.IndexerClient } -func NewTemporaryPrivacyTransferWithAuthorityFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) FulfillmentHandler { - return &TemporaryPrivacyTransferWithAuthorityFulfillmentHandler{ - conf: configProvider(), +func NewCloseEmptyTimelockAccountFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { + return &CloseEmptyTimelockAccountFulfillmentHandler{ data: data, vmIndexerClient: vmIndexerClient, } } -func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.TemporaryPrivacyTransferWithAuthority { +func (h *CloseEmptyTimelockAccountFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { + if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { return false, errors.New("invalid fulfillment type") } - commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return false, err - } - - // Sanity check that we haven't upgraded this private transfer - if commitmentRecord.RepaymentDivertedTo != nil { - return false, nil - } - - // The commitment must be opened before we can send funds to it - if commitmentRecord.State != commitment.StateOpen { - return false, nil - } + // todo: We can have single "AsSourceOrDestination" query - // Check the privacy upgrade deadline, which is one of many factors as to - // why we may have opened the commitment. We need to ensure the deadline - // is hit before proceeding. - privacyUpgradeDeadline, err := commitment_worker.GetDeadlineToUpgradePrivacy(ctx, h.data, commitmentRecord) - if err == commitment_worker.ErrNoPrivacyUpgradeDeadline { - return false, nil - } else if err != nil { + // The source account is a user account, so check that there are no other + // fulfillments where it's used as a source account. + earliestFulfillment, err := h.data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, fulfillmentRecord.Source) + if err != nil && err != fulfillment.ErrFulfillmentNotFound { return false, err } - - // The deadline to upgrade privacy hasn't been met, so don't schedule it - if privacyUpgradeDeadline.After(time.Now()) { - return false, nil - } - - // The source user account is a Code account, so we must validate it exists on - // the blockchain prior to sending funds from it. - isSourceAccountCreated, err := isTokenAccountOnBlockchain(ctx, h.data, fulfillmentRecord.Source) - if err != nil { - return false, err - } else if !isSourceAccountCreated { + if earliestFulfillment != nil && earliestFulfillment.ScheduledBefore(fulfillmentRecord) { return false, nil } - // Check whether there's an earlier fulfillment that should be scheduled first - // where the source user account is the destination. This fulfillment might depend - // on the receipt of some funds. - earliestFulfillmentForSourceAsDestination, err := h.data.GetFirstSchedulableFulfillmentByAddressAsDestination(ctx, fulfillmentRecord.Source) + // The source account is a user account, so check that there are no other + // fulfillments where it's used as a destination account. + earliestFulfillment, err = h.data.GetFirstSchedulableFulfillmentByAddressAsDestination(ctx, fulfillmentRecord.Source) if err != nil && err != fulfillment.ErrFulfillmentNotFound { return false, err } - if earliestFulfillmentForSourceAsDestination != nil && earliestFulfillmentForSourceAsDestination.ScheduledBefore(fulfillmentRecord) { + if earliestFulfillment != nil && earliestFulfillment.ScheduledBefore(fulfillmentRecord) { return false, nil } - recordTemporaryPrivateTransferScheduledEvent(ctx, fulfillmentRecord) return true, nil } -func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) SupportsOnDemandTransactions() bool { +func (h *CloseEmptyTimelockAccountFulfillmentHandler) SupportsOnDemandTransactions() bool { return true } -func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - virtualSignatureBytes, err := base58.Decode(*fulfillmentRecord.VirtualSignature) - if err != nil { - return nil, err - } - - virtualNonce, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.VirtualNonce) - if err != nil { - return nil, err - } - - commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return nil, err - } - - treasuryPool, err := common.NewAccountFromPrivateKeyString(commitmentRecord.Pool) - if err != nil { - return nil, err - } - - treasuryPoolVault, err := common.NewAccountFromPrivateKeyString(*fulfillmentRecord.Destination) - if err != nil { - return nil, err - } - - commitmentVault, err := common.NewAccountFromPublicKeyString(commitmentRecord.VaultAddress) - if err != nil { - return nil, err +func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { + if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { + return nil, errors.New("invalid fulfillment type") } - sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) + timelockVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) if err != nil { return nil, err } - - sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, sourceVault.PublicKey().ToBase58()) + timelockRecord, err := h.data.GetTimelockByVault(ctx, timelockVault.PublicKey().ToBase58()) if err != nil { return nil, err } - - sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) + timelockOwner, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) if err != nil { return nil, err } - _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) + virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, timelockOwner) if err != nil { return nil, err } - _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, sourceAuthority) - if err != nil { - return nil, err + if virtualAccountState.Balance != 0 { + return nil, errors.New("stale timelock account state") } - _, relayMemory, relayIndex, err := getVirtualRelayAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, commitmentVault) + storage, err := reserveVmStorage(ctx, h.data, common.CodeVmAccount, storage.PurposeDeletion, timelockVault) if err != nil { return nil, err } - txn, err := transaction_util.MakeCashChequeTransaction( + txn, err := transaction_util.MakeCompressAccountTransaction( selectedNonce.Account, selectedNonce.Blockhash, - solana.Signature(virtualSignatureBytes), - common.CodeVmAccount, - common.CodeVmOmnibusAccount, - - nonceMemory, - nonceIndex, - sourceMemory, - sourceIndex, - relayMemory, - relayIndex, - - treasuryPool, - treasuryPoolVault, - commitmentRecord.Amount, + memory, + index, + storage, + virtualAccountState.Marshal(), ) if err != nil { return nil, err @@ -752,839 +664,86 @@ func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTr return &txn, nil } -func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.TemporaryPrivacyTransferWithAuthority { +func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { + if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { return errors.New("invalid fulfillment type") } - return savePaymentRecord(ctx, h.data, fulfillmentRecord, txnRecord) + return onVirtualAccountDeleted(ctx, h.data, fulfillmentRecord.Source) } -func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.TemporaryPrivacyTransferWithAuthority { +func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { + if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { return false, errors.New("invalid fulfillment type") } - // This is bad. The treasury pool cannot be refunded + // Fulfillment record needs to be scheduled with a new transaction, which may + // or may not need to be signed by the client. It all depends on whether there + // is dust in the account. + // + // todo: Implement auto-recovery when we know the account is empty + // todo: Do "something" to indicate the client needs to resign a new transaction return false, nil } -func (h *TemporaryPrivacyTransferWithAuthorityFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.TemporaryPrivacyTransferWithAuthority { +func (h *CloseEmptyTimelockAccountFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { + if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { return false, false, errors.New("invalid fulfillment type") } - count, err := h.data.GetFulfillmentCountByTypeActionAndState( - ctx, - fulfillmentRecord.Intent, - fulfillmentRecord.ActionId, - fulfillment.PermanentPrivacyTransferWithAuthority, - fulfillment.StateConfirmed, - ) - if err != nil { - return false, false, err - } - - // Temporary private transfer is revoked when the corresponding permanent - // private transfer in the same action is confirmed. - if count == 0 { - return false, false, nil - } - - nonceRecord, err := h.data.GetNonce(ctx, *fulfillmentRecord.Nonce) - if err != nil { - return false, false, err - } - - // Sanity check because this is dangerous since the blockhash would never be - // progressed and we'd be using a stale one on the next transaction. In an - // ideal world, this points to the upgraded fulfillment or nothing at all. - if nonceRecord.Signature == *fulfillmentRecord.Signature { - return false, false, errors.New("too dangerous to revoke fulfillment") - } - - return true, true, nil -} - -type PermanentPrivacyTransferWithAuthorityFulfillmentHandler struct { - conf *conf - data code_data.Provider - vmIndexerClient indexerpb.IndexerClient -} - -func NewPermanentPrivacyTransferWithAuthorityFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) FulfillmentHandler { - return &PermanentPrivacyTransferWithAuthorityFulfillmentHandler{ - conf: configProvider(), - data: data, - vmIndexerClient: vmIndexerClient, - } + return false, false, nil } -func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.PermanentPrivacyTransferWithAuthority { - return false, errors.New("invalid fulfillment type") - } - - oldCommitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return false, err - } - - // The old commitment record must be marked as diverting funds to the new - // intended commitment before proceeding. - if oldCommitmentRecord.RepaymentDivertedTo == nil { - return false, nil - } - - newCommitmentRecord, err := h.data.GetCommitmentByVault(ctx, *oldCommitmentRecord.RepaymentDivertedTo) - if err != nil { - return false, err - } - - // The commitment vault must be opened before we can send funds to it - if newCommitmentRecord.State != commitment.StateOpen { - return false, nil - } - - // The source user account is a Code account, so we must validate it exists on - // the blockchain prior to sending funds from it. - isSourceAccountCreated, err := isTokenAccountOnBlockchain(ctx, h.data, fulfillmentRecord.Source) - if err != nil { - return false, err - } else if !isSourceAccountCreated { - return false, nil +func isTokenAccountOnBlockchain(ctx context.Context, data code_data.Provider, address string) (bool, error) { + // Optimization for external accounts managed by Code + switch address { + case "Ad4gWGCB94PsA4cP2jqSjfg7eTi4aVkrEdXXhNivT8nW": // Fee collector + return true, nil } - // Check whether there's an earlier fulfillment that should be scheduled first - // where the source user account is the destination. This fulfillment might depend - // on the receipt of some funds. - earliestFulfillmentForSourceAsDestination, err := h.data.GetFirstSchedulableFulfillmentByAddressAsDestination(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { + // Try our cache of Code timelock accounts + timelockRecord, err := data.GetTimelockByVault(ctx, address) + if err == timelock.ErrTimelockNotFound { + // Likely not a Code timelock account, so defer to the blockchain + _, err := data.GetBlockchainTokenAccountInfo(ctx, address, solana.CommitmentFinalized) + if err == solana.ErrNoAccountInfo || err == token.ErrAccountNotFound { + return false, nil + } else if err != nil { + return false, err + } + return true, nil + } else if err != nil { return false, err } - if earliestFulfillmentForSourceAsDestination != nil && earliestFulfillmentForSourceAsDestination.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - return true, nil -} - -func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) SupportsOnDemandTransactions() bool { - return true -} - -func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - virtualSignatureBytes, err := base58.Decode(*fulfillmentRecord.VirtualSignature) - if err != nil { - return nil, err - } - virtualNonce, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.VirtualNonce) - if err != nil { - return nil, err - } - - oldCommitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return nil, err - } - - newCommitmentRecord, err := h.data.GetCommitmentByVault(ctx, *oldCommitmentRecord.RepaymentDivertedTo) - if err != nil { - return nil, err - } - - treasuryPool, err := common.NewAccountFromPrivateKeyString(newCommitmentRecord.Pool) - if err != nil { - return nil, err - } - - treasuryPoolVault, err := common.NewAccountFromPrivateKeyString(*fulfillmentRecord.Destination) - if err != nil { - return nil, err - } - - commitmentVault, err := common.NewAccountFromPublicKeyString(newCommitmentRecord.VaultAddress) - if err != nil { - return nil, err - } - - sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) - if err != nil { - return nil, err - } - - sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, sourceVault.PublicKey().ToBase58()) - if err != nil { - return nil, err - } - - sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) - if err != nil { - return nil, err - } + existsOnBlockchain := timelockRecord.ExistsOnBlockchain() - _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) - if err != nil { - return nil, err - } + // We've detected the use of an account that's not on the blockchain, so + // best-effort kick off active scheduling for the InitializeLockedTimelockAccount + // fulfillment. + if !existsOnBlockchain { + // Initializing an account is always the first thing scheduled + initializeFulfillmentRecord, err := data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, address) + if err != nil { + return existsOnBlockchain, nil + } - _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, sourceAuthority) - if err != nil { - return nil, err - } + if initializeFulfillmentRecord.FulfillmentType != fulfillment.InitializeLockedTimelockAccount { + return existsOnBlockchain, nil + } - _, relayMemory, relayIndex, err := getVirtualRelayAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, commitmentVault) - if err != nil { - return nil, err - } - - txn, err := transaction_util.MakeCashChequeTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, - - solana.Signature(virtualSignatureBytes), - - common.CodeVmAccount, - common.CodeVmOmnibusAccount, - - nonceMemory, - nonceIndex, - sourceMemory, - sourceIndex, - relayMemory, - relayIndex, - - treasuryPool, - treasuryPoolVault, - oldCommitmentRecord.Amount, - ) - if err != nil { - return nil, err - } - return &txn, nil -} - -func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.PermanentPrivacyTransferWithAuthority { - return errors.New("invalid fulfillment type") - } - - err := savePaymentRecord(ctx, h.data, fulfillmentRecord, txnRecord) - if err != nil { - return err - } - - // Wake up the temporary privacy transaction so we can process it to a revoked state - temporaryTransferFulfillment, err := h.data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return err - } else if err == nil { - return markFulfillmentAsActivelyScheduled(ctx, h.data, temporaryTransferFulfillment[0]) - } - return nil -} - -func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.PermanentPrivacyTransferWithAuthority { - return false, errors.New("invalid fulfillment type") - } - - // This is bad. The treasury pool cannot be refunded - return false, nil -} - -func (h *PermanentPrivacyTransferWithAuthorityFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.PermanentPrivacyTransferWithAuthority { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil -} - -type TransferWithCommitmentFulfillmentHandler struct { - data code_data.Provider - vmIndexerClient indexerpb.IndexerClient -} - -func NewTransferWithCommitmentFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { - return &TransferWithCommitmentFulfillmentHandler{ - data: data, - vmIndexerClient: vmIndexerClient, - } -} - -func (h *TransferWithCommitmentFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.TransferWithCommitment { - return false, errors.New("invalid fulfillment type") - } - - // Ensure the commitment record exists and it's in a valid initial state. - commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return false, err - } else if commitmentRecord.State != commitment.StateUnknown && commitmentRecord.State != commitment.StatePayingDestination { - return false, errors.New("commitment in unexpected state") - } - - // The destination account is a Code account, so we must validate it exists - // on the blockchain prior to sending funds to it. - isDestinationAccountCreated, err := isTokenAccountOnBlockchain(ctx, h.data, *fulfillmentRecord.Destination) - if err != nil { - return false, err - } else if !isDestinationAccountCreated { - return false, nil - } - - // If our funds aren't already reserved for use with the treasury pool, then we - // need to check if there's sufficient funding to pay the destination. - if commitmentRecord.State != commitment.StatePayingDestination { - // No need to include the state transition in the lock yet, since we transition - // the commitment account to a state where the funds will be reserved. If the DB - // has a failure, we'll just retry scheduling and it will go through the next - // time. - treasuryPoolLock.Lock() - defer treasuryPoolLock.Unlock() - - poolRecord, err := h.data.GetTreasuryPoolByAddress(ctx, commitmentRecord.Pool) - if err != nil { - return false, err - } - - totalAvailableTreasuryPoolFunds, usedTreasuryPoolFunds, err := estimateTreasuryPoolFundingLevels(ctx, h.data, poolRecord) - if err != nil { - return false, err - } - - // The treasury pool's funds are used entirely - if usedTreasuryPoolFunds >= totalAvailableTreasuryPoolFunds { - return false, nil - } - - // The treasury pool doesn't have sufficient funds to transfer to the destination - // account. - remainingTreasuryPoolFunds := totalAvailableTreasuryPoolFunds - usedTreasuryPoolFunds - if remainingTreasuryPoolFunds < commitmentRecord.Amount { - return false, nil - } - - // Mark the commitment as paying the destination, so we can track the funds we're - // going to be using from the treasury pool. - err = markCommitmentPayingDestination(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return false, err - } - } - - return true, nil -} - -func (h *TransferWithCommitmentFulfillmentHandler) SupportsOnDemandTransactions() bool { - return true -} - -func (h *TransferWithCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return nil, err - } - - if commitmentRecord.State != commitment.StatePayingDestination { - return nil, errors.New("commitment in unexpected state") - } - - treasuryPool, err := common.NewAccountFromPublicKeyString(commitmentRecord.Pool) - if err != nil { - return nil, err - } - - treasuryPoolVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) - if err != nil { - return nil, err - } - - destination, err := common.NewAccountFromPublicKeyString(commitmentRecord.Destination) - if err != nil { - return nil, err - } - - commitment, err := common.NewAccountFromPublicKeyString(commitmentRecord.Address) - if err != nil { - return nil, err - } - - commitmentVault, err := common.NewAccountFromPublicKeyString(commitmentRecord.VaultAddress) - if err != nil { - return nil, err - } - - transcript, err := hex.DecodeString(commitmentRecord.Transcript) - if err != nil { - return nil, err - } - - recentRoot, err := hex.DecodeString(commitmentRecord.RecentRoot) - if err != nil { - return nil, err - } - - relayMemory, relayAccountIndex, err := reserveVmMemory(ctx, h.data, common.CodeVmAccount, cvm.VirtualAccountTypeRelay, commitmentVault) - if err != nil { - return nil, err - } - - isInternal, err := isInternalVmTransfer(ctx, h.data, destination) - if err != nil { - return nil, err - } - - var txn solana.Transaction - var makeTxnErr error - if isInternal { - destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, commitmentRecord.Destination) - if err != nil { - return nil, err - } - - destinationOwner, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) - if err != nil { - return nil, err - } - - _, timelockAccountMemory, timelockAccountIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationOwner) - if err != nil { - return nil, err - } - - txn, makeTxnErr = transaction_util.MakeInternalTreasuryAdvanceTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, - - common.CodeVmAccount, - timelockAccountMemory, - timelockAccountIndex, - relayMemory, - relayAccountIndex, - - treasuryPool, - treasuryPoolVault, - commitment, - commitmentRecord.Amount, - transcript, - recentRoot, - ) - } else { - txn, makeTxnErr = transaction_util.MakeExternalTreasuryAdvanceTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, - - common.CodeVmAccount, - relayMemory, - relayAccountIndex, - - treasuryPool, - treasuryPoolVault, - destination, - commitment, - commitmentRecord.Amount, - transcript, - recentRoot, - ) - } - if makeTxnErr != nil { - return nil, makeTxnErr - } - return &txn, nil -} - -func (h *TransferWithCommitmentFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.TransferWithCommitment { - return errors.New("invalid fulfillment type") - } - - err := savePaymentRecord(ctx, h.data, fulfillmentRecord, txnRecord) - if err != nil { - return err - } - - return markCommitmentOpen(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) -} - -func (h *TransferWithCommitmentFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.TransferWithCommitment { - return false, errors.New("invalid fulfillment type") - } - - // Fulfillment record needs to be scheduled with a new transaction - // - // todo: Implement auto-recovery - return false, nil -} - -func (h *TransferWithCommitmentFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.TransferWithCommitment { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil -} - -type CloseEmptyTimelockAccountFulfillmentHandler struct { - data code_data.Provider - vmIndexerClient indexerpb.IndexerClient -} - -func NewCloseEmptyTimelockAccountFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { - return &CloseEmptyTimelockAccountFulfillmentHandler{ - data: data, - vmIndexerClient: vmIndexerClient, - } -} - -func (h *CloseEmptyTimelockAccountFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { - return false, errors.New("invalid fulfillment type") - } - - // todo: We can have single "AsSourceOrDestination" query - - // The source account is a user account, so check that there are no other - // fulfillments where it's used as a source account. - earliestFulfillment, err := h.data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return false, err - } - if earliestFulfillment != nil && earliestFulfillment.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - // The source account is a user account, so check that there are no other - // fulfillments where it's used as a destination account. - earliestFulfillment, err = h.data.GetFirstSchedulableFulfillmentByAddressAsDestination(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return false, err - } - if earliestFulfillment != nil && earliestFulfillment.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - return true, nil -} - -func (h *CloseEmptyTimelockAccountFulfillmentHandler) SupportsOnDemandTransactions() bool { - return true -} - -func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { - return nil, errors.New("invalid fulfillment type") - } - - timelockVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) - if err != nil { - return nil, err - } - timelockRecord, err := h.data.GetTimelockByVault(ctx, timelockVault.PublicKey().ToBase58()) - if err != nil { - return nil, err - } - timelockOwner, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) - if err != nil { - return nil, err - } - - virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, timelockOwner) - if err != nil { - return nil, err - } - - if virtualAccountState.Balance != 0 { - return nil, errors.New("stale timelock account state") - } - - storage, err := reserveVmStorage(ctx, h.data, common.CodeVmAccount, storage.PurposeDeletion, timelockVault) - if err != nil { - return nil, err - } - - txn, err := transaction_util.MakeCompressAccountTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, - - common.CodeVmAccount, - memory, - index, - storage, - virtualAccountState.Marshal(), - ) - if err != nil { - return nil, err - } - return &txn, nil -} - -func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { - return errors.New("invalid fulfillment type") - } - - return onVirtualAccountDeleted(ctx, h.data, fulfillmentRecord.Source) -} - -func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { - return false, errors.New("invalid fulfillment type") - } - - // Fulfillment record needs to be scheduled with a new transaction, which may - // or may not need to be signed by the client. It all depends on whether there - // is dust in the account. - // - // todo: Implement auto-recovery when we know the account is empty - // todo: Do "something" to indicate the client needs to resign a new transaction - return false, nil -} - -func (h *CloseEmptyTimelockAccountFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil -} - -type SaveRecentRootFulfillmentHandler struct { - data code_data.Provider -} - -func NewSaveRecentRootFulfillmentHandler(data code_data.Provider) FulfillmentHandler { - return &SaveRecentRootFulfillmentHandler{ - data: data, - } -} - -// Assumption: Saving a recent root is pre-sorted to the back of the line -func (h *SaveRecentRootFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.SaveRecentRoot { - return false, errors.New("invalid fulfillment type") - } - - // Ensure that any prior TransferWithCommitment fulfillments are played out - // before saving the recent root. This will ensure the scheduler itself won't - // go too fast and risk having outdated recent roots. It's not perfect, and - // it's still up to the process making SaveRecentRoot intents to determine - // when it's safe to do so. - // - // Note: The treasury is only the source of funds for TransferWithCommitment transactions. - earliestTransferWithCommitment, err := h.data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, fulfillmentRecord.Source) - if err != nil && err != fulfillment.ErrFulfillmentNotFound { - return false, err - } - if earliestTransferWithCommitment != nil && earliestTransferWithCommitment.ScheduledBefore(fulfillmentRecord) { - return false, nil - } - - return true, nil -} - -func (h *SaveRecentRootFulfillmentHandler) SupportsOnDemandTransactions() bool { - return false -} - -func (h *SaveRecentRootFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - return nil, errors.New("not supported") -} - -func (h *SaveRecentRootFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.SaveRecentRoot { - return errors.New("invalid fulfillment type") - } - - return nil -} - -func (h *SaveRecentRootFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.SaveRecentRoot { - return false, errors.New("invalid fulfillment type") - } - - // Fulfillment record needs to be scheduled with a new transaction - // - // todo: Implement auto-recovery - return false, nil -} - -func (h *SaveRecentRootFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.SaveRecentRoot { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil -} - -type CloseCommitmentFulfillmentHandler struct { - data code_data.Provider - vmIndexerClient indexerpb.IndexerClient -} - -func NewCloseCommitmentFulfillmentHandler(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient) FulfillmentHandler { - return &CloseCommitmentFulfillmentHandler{ - data: data, - vmIndexerClient: vmIndexerClient, - } -} - -// todo: New commitment closing flow not implemented yet -func (h *CloseCommitmentFulfillmentHandler) CanSubmitToBlockchain(ctx context.Context, fulfillmentRecord *fulfillment.Record) (scheduled bool, err error) { - commitmentRecord, err := h.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return false, err - } - - // Commitment worker guarantees all cheques have been cashed - return commitmentRecord.State == commitment.StateClosing, nil -} - -func (h *CloseCommitmentFulfillmentHandler) SupportsOnDemandTransactions() bool { - return true -} - -func (h *CloseCommitmentFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.SelectedNonce) (*solana.Transaction, error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { - return nil, errors.New("invalid fulfillment type") - } - - relay, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) - if err != nil { - return nil, err - } - - virtualAccountState, memory, index, err := getVirtualRelayAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, relay) - if err != nil { - return nil, err - } - - storage, err := reserveVmStorage(ctx, h.data, common.CodeVmAccount, storage.PurposeDeletion, relay) - if err != nil { - return nil, err - } - - txn, err := transaction_util.MakeCompressAccountTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, - - common.CodeVmAccount, - memory, - index, - storage, - virtualAccountState.Marshal(), - ) - if err != nil { - return nil, err - } - return &txn, nil -} - -func (h *CloseCommitmentFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { - return errors.New("invalid fulfillment type") - } - - err := markCommitmentClosed(ctx, h.data, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return err - } - - return onVirtualAccountDeleted(ctx, h.data, fulfillmentRecord.Source) -} - -func (h *CloseCommitmentFulfillmentHandler) OnFailure(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) (recovered bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { - return false, errors.New("invalid fulfillment type") - } - - // Let it fail. More than likely we have a bug. In theory, we could try implementing - // auto-recovery. - return false, nil -} - -func (h *CloseCommitmentFulfillmentHandler) IsRevoked(ctx context.Context, fulfillmentRecord *fulfillment.Record) (revoked bool, nonceUsed bool, err error) { - if fulfillmentRecord.FulfillmentType != fulfillment.CloseCommitment { - return false, false, errors.New("invalid fulfillment type") - } - - return false, false, nil -} - -func isTokenAccountOnBlockchain(ctx context.Context, data code_data.Provider, address string) (bool, error) { - // Optimization for external accounts managed by Code - switch address { - case "Ad4gWGCB94PsA4cP2jqSjfg7eTi4aVkrEdXXhNivT8nW": // Fee collector - return true, nil - } - - // Try our cache of Code timelock accounts - timelockRecord, err := data.GetTimelockByVault(ctx, address) - if err == timelock.ErrTimelockNotFound { - // Likely not a Code timelock account, so defer to the blockchain - _, err := data.GetBlockchainTokenAccountInfo(ctx, address, solana.CommitmentFinalized) - if err == solana.ErrNoAccountInfo || err == token.ErrAccountNotFound { - return false, nil - } else if err != nil { - return false, err - } - return true, nil - } else if err != nil { - return false, err - } - - existsOnBlockchain := timelockRecord.ExistsOnBlockchain() - - // We've detected the use of an account that's not on the blockchain, so - // best-effort kick off active scheduling for the InitializeLockedTimelockAccount - // fulfillment. - if !existsOnBlockchain { - // Initializing an account is always the first thing scheduled - initializeFulfillmentRecord, err := data.GetFirstSchedulableFulfillmentByAddressAsSource(ctx, address) - if err != nil { - return existsOnBlockchain, nil - } - - if initializeFulfillmentRecord.FulfillmentType != fulfillment.InitializeLockedTimelockAccount { - return existsOnBlockchain, nil - } - - markFulfillmentAsActivelyScheduled(ctx, data, initializeFulfillmentRecord) + markFulfillmentAsActivelyScheduled(ctx, data, initializeFulfillmentRecord) } return existsOnBlockchain, nil } -func estimateTreasuryPoolFundingLevels(ctx context.Context, data code_data.Provider, record *treasury.Record) (total uint64, used uint64, err error) { - total, err = data.GetTotalAvailableTreasuryPoolFunds(ctx, record.Vault) - if err != nil { - return 0, 0, err - } - - used, err = data.GetUsedTreasuryPoolDeficitFromCommitments(ctx, record.Address) - if err != nil { - return 0, 0, err - } - - return total, used, nil -} - // todo: simplify initialization of fulfillment handlers across service and contextual scheduler func getFulfillmentHandlers(data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, configProvider ConfigProvider) map[fulfillment.Type]FulfillmentHandler { handlersByType := make(map[fulfillment.Type]FulfillmentHandler) handlersByType[fulfillment.InitializeLockedTimelockAccount] = NewInitializeLockedTimelockAccountFulfillmentHandler(data) handlersByType[fulfillment.NoPrivacyTransferWithAuthority] = NewNoPrivacyTransferWithAuthorityFulfillmentHandler(data, vmIndexerClient) handlersByType[fulfillment.NoPrivacyWithdraw] = NewNoPrivacyWithdrawFulfillmentHandler(data, vmIndexerClient) - handlersByType[fulfillment.TemporaryPrivacyTransferWithAuthority] = NewTemporaryPrivacyTransferWithAuthorityFulfillmentHandler(data, vmIndexerClient, configProvider) - handlersByType[fulfillment.PermanentPrivacyTransferWithAuthority] = NewPermanentPrivacyTransferWithAuthorityFulfillmentHandler(data, vmIndexerClient, configProvider) - handlersByType[fulfillment.TransferWithCommitment] = NewTransferWithCommitmentFulfillmentHandler(data, vmIndexerClient) handlersByType[fulfillment.CloseEmptyTimelockAccount] = NewCloseEmptyTimelockAccountFulfillmentHandler(data, vmIndexerClient) - handlersByType[fulfillment.SaveRecentRoot] = NewSaveRecentRootFulfillmentHandler(data) - handlersByType[fulfillment.CloseCommitment] = NewCloseCommitmentFulfillmentHandler(data, vmIndexerClient) return handlersByType } diff --git a/pkg/code/async/sequencer/intent_handler.go b/pkg/code/async/sequencer/intent_handler.go index fb9e06f2..4c69b3f6 100644 --- a/pkg/code/async/sequencer/intent_handler.go +++ b/pkg/code/async/sequencer/intent_handler.go @@ -6,7 +6,6 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" ) @@ -57,330 +56,12 @@ func (h *OpenAccountsIntentHandler) OnActionUpdated(ctx context.Context, intentI return markIntentConfirmed(ctx, h.data, intentId) } -type SendPrivatePaymentIntentHandler struct { - data code_data.Provider -} - -func NewSendPrivatePaymentIntentHandler(data code_data.Provider) IntentHandler { - return &SendPrivatePaymentIntentHandler{ - data: data, - } -} - -func (h *SendPrivatePaymentIntentHandler) OnActionUpdated(ctx context.Context, intentId string) error { - actionRecords, err := h.data.GetAllActionsByIntent(ctx, intentId) - if err != nil { - return err - } - - canMarkConfirmed := true - for _, actionRecord := range actionRecords { - switch actionRecord.ActionType { - case action.PrivateTransfer, action.NoPrivacyTransfer, action.NoPrivacyWithdraw: - default: - continue - } - - // Intent is failed if at least one money movement action fails - if actionRecord.State == action.StateFailed { - return markIntentFailed(ctx, h.data, intentId) - } - - if actionRecord.State != action.StateConfirmed { - canMarkConfirmed = false - } - } - - // Intent is confirmed when all money movement actions are confirmed - if canMarkConfirmed { - return markIntentConfirmed(ctx, h.data, intentId) - } - return h.maybeMarkTempOutgoingAccountActionsAsActivelyScheduled(ctx, intentId, actionRecords) -} - -func (h *SendPrivatePaymentIntentHandler) maybeMarkTempOutgoingAccountActionsAsActivelyScheduled(ctx context.Context, intentId string, actionsRecords []*action.Record) error { - intentRecord, err := h.data.GetIntent(ctx, intentId) - if err != nil { - return err - } - - // Find relevant actions that have fulfillments using the temp outgoing account - // where active scheduling is disabled because of treasury advance dependencies. - // - // Note: SubmitIntent validation guarantees there's a single NoPrivacyWithdraw - // and NoPrivacyTransfer action that maps to the payment to the destianattion - // account and optional fee to Code, respectively, all coming from the temp - // outgoing account. - var paymentToDestinationAction *action.Record - var feePaymentActions []*action.Record - for _, actionRecord := range actionsRecords { - switch actionRecord.ActionType { - case action.NoPrivacyWithdraw: - paymentToDestinationAction = actionRecord - case action.NoPrivacyTransfer: - feePaymentActions = append(feePaymentActions, actionRecord) - } - } - - if paymentToDestinationAction == nil { - return errors.New("payment to destination action not found") - } - if len(feePaymentActions) == 0 && intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - return errors.New("fee payment action not found") - } - - // Extract the corresponding fulfillment records that have active scheduling - // disabled. - - var paymentToDestinationFulfillment *fulfillment.Record - fulfillmentRecords, err := h.data.GetAllFulfillmentsByTypeAndAction( - ctx, - fulfillment.NoPrivacyWithdraw, - intentId, - paymentToDestinationAction.ActionId, - ) - if err != nil { - return err - } else if err == nil && fulfillmentRecords[0].DisableActiveScheduling { - paymentToDestinationFulfillment = fulfillmentRecords[0] - } - - var feePaymentFulfillments []*fulfillment.Record - for _, feePaymentAction := range feePaymentActions { - fulfillmentRecords, err := h.data.GetAllFulfillmentsByTypeAndAction( - ctx, - fulfillment.NoPrivacyTransferWithAuthority, - intentId, - feePaymentAction.ActionId, - ) - if err != nil { - return err - } else if err == nil && fulfillmentRecords[0].DisableActiveScheduling { - feePaymentFulfillments = append(feePaymentFulfillments, fulfillmentRecords[0]) - } - } - - // Short circuit if there's nothing to update to avoid redundant intent - // state checks spanning all actions. - if paymentToDestinationFulfillment == nil && len(feePaymentFulfillments) == 0 { - return nil - } - - // Do some sanity checks to determine whether active scheduling can be enabled. - tempOutgoingAccount := paymentToDestinationAction.Source - for _, actionRecord := range actionsRecords { - if actionRecord.ActionType != action.PrivateTransfer { - continue - } - - if *actionRecord.Destination != tempOutgoingAccount { - continue - } - - // Is there a treasury advance that's sending funds to the temp outgoing - // account that's not pending or completed? If so, wait for all advances - // to be scheduled or completed. We need to rely on fulfillments because - // private transfer action states operate on the entire lifecycle, and we - // only care about the treasury advances. - transferWithCommitmentFulfillment, err := h.data.GetAllFulfillmentsByTypeAndAction( - ctx, - fulfillment.TransferWithCommitment, - intentId, - actionRecord.ActionId, - ) - if err != nil { - return err - } - - // Note: Due to how the generic fulfillment worker logic functions, it's - // likely that at least one fulfillment is in an in flux state from pending - // to confirmed. This is by design to allow the worker to retry, but makes - // this logic imperfect by just checking for all confirmed. That's why it - // differs from other intent handlers that can operate on actions, since - // that flow has these guarantees. Regardless, dependency logic still saves - // us and we're only making the fulfillment available for active polling. - if transferWithCommitmentFulfillment[0].State != fulfillment.StatePending && transferWithCommitmentFulfillment[0].State != fulfillment.StateConfirmed { - return nil - } - } - - // Mark fulfillments as actively scheduled when at least all treasury payments - // to the temp outgoing account are in flight. - for _, feePaymentFulfillment := range feePaymentFulfillments { - err = markFulfillmentAsActivelyScheduled(ctx, h.data, feePaymentFulfillment) - if err != nil { - return err - } - } - if paymentToDestinationFulfillment != nil { - err = markFulfillmentAsActivelyScheduled(ctx, h.data, paymentToDestinationFulfillment) - if err != nil { - return err - } - } - return nil -} - -type ReceivePaymentsPrivatelyIntentHandler struct { - data code_data.Provider -} - -func NewReceivePaymentsPrivatelyIntentHandler(data code_data.Provider) IntentHandler { - return &ReceivePaymentsPrivatelyIntentHandler{ - data: data, - } -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) OnActionUpdated(ctx context.Context, intentId string) error { - actionRecords, err := h.data.GetAllActionsByIntent(ctx, intentId) - if err != nil { - return err - } - - canMarkConfirmed := true - for _, actionRecord := range actionRecords { - switch actionRecord.ActionType { - case action.PrivateTransfer, action.CloseEmptyAccount: - default: - continue - } - - // Intent is failed if at least one PrivateTransfer action fails - if actionRecord.State == action.StateFailed { - return markIntentFailed(ctx, h.data, intentId) - } - - if actionRecord.State != action.StateConfirmed { - canMarkConfirmed = false - } - } - - // Intent is confirmed when all PrivateTransfer and CloseEmptyAccount (there should only - // be one when receiving from temp incoming) actions are confirmed - if canMarkConfirmed { - return markIntentConfirmed(ctx, h.data, intentId) - } - return h.maybeMarkCloseEmptyAccountActionAsActivelyScheduled(ctx, intentId, actionRecords) -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) maybeMarkCloseEmptyAccountActionAsActivelyScheduled(ctx context.Context, intentId string, actionsRecords []*action.Record) error { - intentRecord, err := h.data.GetIntent(ctx, intentId) - if err != nil { - return err - } - - // Deposits don't have a CloseEmptyAccount action because they receive from a - // persistent primary account - if intentRecord.ReceivePaymentsPrivatelyMetadata.IsDeposit { - return nil - } - - var closeEmptyAccountAction *action.Record - for _, actionRecord := range actionsRecords { - if actionRecord.ActionType == action.CloseEmptyAccount { - closeEmptyAccountAction = actionRecord - break - } - } - - if closeEmptyAccountAction == nil { - return errors.New("close empty account action not found") - } - - tempIncomingAccount := closeEmptyAccountAction.Source - - // Is there an unconfirmed private transfer that's dependent on the account - // being closed as a source of funds? If so, wait for it to complete to drain - // the balance. - for _, actionRecord := range actionsRecords { - if actionRecord.ActionType != action.PrivateTransfer { - continue - } - - if actionRecord.Source != tempIncomingAccount { - continue - } - - if actionRecord.State != action.StateConfirmed { - return nil - } - } - - // All private transfers from the temp incoming account are confirmed. There - // should be no funds (except possibly some dust), so w can actively schedule - // to close fulfillment. - - // There should only be one per intent validation in SubmitIntent - closeEmptyAccountFulfillment, err := h.data.GetAllFulfillmentsByTypeAndAction( - ctx, - fulfillment.CloseEmptyTimelockAccount, - closeEmptyAccountAction.Intent, - closeEmptyAccountAction.ActionId, - ) - if err != nil { - return err - } - return markFulfillmentAsActivelyScheduled(ctx, h.data, closeEmptyAccountFulfillment[0]) -} - -type SaveRecentRootIntentHandler struct { - data code_data.Provider -} - -func NewSaveRecentRootIntentHandler(data code_data.Provider) IntentHandler { - return &SaveRecentRootIntentHandler{ - data: data, - } -} - -func (h *SaveRecentRootIntentHandler) OnActionUpdated(ctx context.Context, intentId string) error { - actionRecord, err := h.data.GetActionById(ctx, intentId, 0) - if err != nil { - return err - } - - // Intent is confirmed/failed based on the state the single action - switch actionRecord.State { - case action.StateConfirmed: - return markIntentConfirmed(ctx, h.data, intentId) - case action.StateFailed: - return markIntentFailed(ctx, h.data, intentId) - } - return nil -} - -type MigrateToPrivacy2022IntentHandler struct { - data code_data.Provider -} - -func NewMigrateToPrivacy2022IntentHandler(data code_data.Provider) IntentHandler { - return &MigrateToPrivacy2022IntentHandler{ - data: data, - } -} - -func (h *MigrateToPrivacy2022IntentHandler) OnActionUpdated(ctx context.Context, intentId string) error { - actionRecord, err := h.data.GetActionById(ctx, intentId, 0) - if err != nil { - return err - } - - // Intent is confirmed/failed based on the state the single action - switch actionRecord.State { - case action.StateConfirmed: - return markIntentConfirmed(ctx, h.data, intentId) - case action.StateFailed: - return markIntentFailed(ctx, h.data, intentId) - } - return nil -} - type SendPublicPaymentIntentHandler struct { data code_data.Provider } func NewSendPublicPaymentIntentHandler(data code_data.Provider) IntentHandler { - return &MigrateToPrivacy2022IntentHandler{ + return &SendPublicPaymentIntentHandler{ data: data, } } @@ -427,32 +108,6 @@ func (h *ReceivePaymentsPubliclyIntentHandler) OnActionUpdated(ctx context.Conte return nil } -type EstablishRelationshipIntentHandler struct { - data code_data.Provider -} - -func NewEstablishRelationshipIntentHandler(data code_data.Provider) IntentHandler { - return &EstablishRelationshipIntentHandler{ - data: data, - } -} - -func (h *EstablishRelationshipIntentHandler) OnActionUpdated(ctx context.Context, intentId string) error { - actionRecord, err := h.data.GetActionById(ctx, intentId, 0) - if err != nil { - return err - } - - // Intent is confirmed/failed based on the state the single action - switch actionRecord.State { - case action.StateConfirmed: - return markIntentConfirmed(ctx, h.data, intentId) - case action.StateFailed: - return markIntentFailed(ctx, h.data, intentId) - } - return nil -} - func validateIntentState(record *intent.Record, states ...intent.State) error { for _, validState := range states { if record.State == validState { @@ -502,12 +157,7 @@ func markIntentFailed(ctx context.Context, data code_data.Provider, intentId str func getIntentHandlers(data code_data.Provider) map[intent.Type]IntentHandler { handlersByType := make(map[intent.Type]IntentHandler) handlersByType[intent.OpenAccounts] = NewOpenAccountsIntentHandler(data) - handlersByType[intent.SendPrivatePayment] = NewSendPrivatePaymentIntentHandler(data) - handlersByType[intent.ReceivePaymentsPrivately] = NewReceivePaymentsPrivatelyIntentHandler(data) - handlersByType[intent.SaveRecentRoot] = NewSaveRecentRootIntentHandler(data) - handlersByType[intent.MigrateToPrivacy2022] = NewMigrateToPrivacy2022IntentHandler(data) handlersByType[intent.SendPublicPayment] = NewSendPublicPaymentIntentHandler(data) handlersByType[intent.ReceivePaymentsPublicly] = NewReceivePaymentsPubliclyIntentHandler(data) - handlersByType[intent.EstablishRelationship] = NewEstablishRelationshipIntentHandler(data) return handlersByType } diff --git a/pkg/code/async/sequencer/payment.go b/pkg/code/async/sequencer/payment.go deleted file mode 100644 index 356dffb5..00000000 --- a/pkg/code/async/sequencer/payment.go +++ /dev/null @@ -1,77 +0,0 @@ -package async_sequencer - -import ( - "context" - "errors" - "time" - - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/payment" - "github.com/code-payments/code-server/pkg/code/data/transaction" -) - -func savePaymentRecord(ctx context.Context, data code_data.Provider, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { - var transactionIndex uint32 - switch fulfillmentRecord.FulfillmentType { - case fulfillment.NoPrivacyWithdraw: - transactionIndex = 4 - case fulfillment.TemporaryPrivacyTransferWithAuthority, - fulfillment.PermanentPrivacyTransferWithAuthority, - fulfillment.TransferWithCommitment, - fulfillment.NoPrivacyTransferWithAuthority: - transactionIndex = 2 - default: - return errors.New("cannot save payment for fulfillment type") - } - - actionRecord, err := data.GetActionById(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) - if err != nil { - return err - } - - if txnRecord.HasErrors || txnRecord.ConfirmationState == transaction.ConfirmationFailed { - return errors.New("cannot save a failed payment") - } - - if txnRecord.ConfirmationState != transaction.ConfirmationFinalized { - return errors.New("cannot save a payment that hasn't finalized") - } - - usdExchangeRecord, err := data.GetExchangeRate(ctx, currency_lib.USD, time.Now()) - if err != nil { - return err - } - - paymentRecord := &payment.Record{ - // Source and destination should come from fulfillment, since intent or action - // don't include the intermediary steps between accounts that aren't user accounts. - Source: fulfillmentRecord.Source, - Destination: *fulfillmentRecord.Destination, - - // todo: Assumes we don't split payments across multiple transactions in an action - Quantity: *actionRecord.Quantity, - - // todo: Just filling this in with KIN currency and latest USD rate. I don't think - // these make sense in payment records anymore. These details are captured by - // intents. - ExchangeCurrency: string(currency_lib.KIN), - ExchangeRate: 1.0, - UsdMarketValue: usdExchangeRecord.Rate * float64(kin.FromQuarks(*actionRecord.Quantity)), - - IsExternal: false, - Rendezvous: fulfillmentRecord.Intent, - - TransactionId: txnRecord.Signature, - TransactionIndex: transactionIndex, - BlockId: txnRecord.Slot, - BlockTime: txnRecord.BlockTime, - - ConfirmationState: transaction.ConfirmationFinalized, - - CreatedAt: time.Now(), - } - return data.UpdateOrCreatePayment(ctx, paymentRecord) -} diff --git a/pkg/code/async/treasury/config.go b/pkg/code/async/treasury/config.go deleted file mode 100644 index b548953a..00000000 --- a/pkg/code/async/treasury/config.go +++ /dev/null @@ -1,60 +0,0 @@ -package async_treasury - -import ( - "time" - - "github.com/code-payments/code-server/pkg/config" - "github.com/code-payments/code-server/pkg/config/env" - "github.com/code-payments/code-server/pkg/config/memory" - "github.com/code-payments/code-server/pkg/config/wrapper" -) - -const ( - envConfigPrefix = "TREASURY_SERVICE_" - - HideInCrowdPrivacyLevelConfigEnvName = envConfigPrefix + "HIDE_IN_CROWD_PRIVACY_LEVEL" - defaultHideInCrowdPrivacylevel = 10 - - AdvanceCollectionTimeoutConfigEnvName = envConfigPrefix + "ADVANCE_COLLECTION_TIMEOUT" - defaultAdvanceCollectionTimeout = 24 * time.Hour -) - -type conf struct { - hideInCrowdPrivacyLevel config.Uint64 - advanceCollectionTimeout config.Duration -} - -// ConfigProvider defines how config values are pulled -type ConfigProvider func() *conf - -// WithEnvConfigs returns configuration pulled from environment variables -func WithEnvConfigs() ConfigProvider { - return func() *conf { - return &conf{ - hideInCrowdPrivacyLevel: env.NewUint64Config(HideInCrowdPrivacyLevelConfigEnvName, defaultHideInCrowdPrivacylevel), - advanceCollectionTimeout: env.NewDurationConfig(AdvanceCollectionTimeoutConfigEnvName, defaultAdvanceCollectionTimeout), - } - } -} - -type testOverrides struct { - hideInTheCrowdPrivacyLevel uint64 - advanceCollectionTimeout time.Duration -} - -func withManualTestOverrides(overrides *testOverrides) ConfigProvider { - if overrides.hideInTheCrowdPrivacyLevel == 0 { - overrides.hideInTheCrowdPrivacyLevel = defaultHideInCrowdPrivacylevel - } - - if overrides.advanceCollectionTimeout == 0 { - overrides.advanceCollectionTimeout = defaultAdvanceCollectionTimeout - } - - return func() *conf { - return &conf{ - hideInCrowdPrivacyLevel: wrapper.NewUint64Config(memory.NewConfig(overrides.hideInTheCrowdPrivacyLevel), defaultHideInCrowdPrivacylevel), - advanceCollectionTimeout: wrapper.NewDurationConfig(memory.NewConfig(overrides.advanceCollectionTimeout), defaultAdvanceCollectionTimeout), - } - } -} diff --git a/pkg/code/async/treasury/merkle_tree.go b/pkg/code/async/treasury/merkle_tree.go deleted file mode 100644 index 448e7f06..00000000 --- a/pkg/code/async/treasury/merkle_tree.go +++ /dev/null @@ -1,458 +0,0 @@ -package async_treasury - -import ( - "context" - "sort" - - "github.com/mr-tron/base58" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/solana" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/merkletree" - "github.com/code-payments/code-server/pkg/code/data/payment" - "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/code-payments/code-server/pkg/code/data/treasury" -) - -func (p *service) syncMerkleTree(ctx context.Context, treasuryPoolRecord *treasury.Record) error { - log := p.log.WithFields(logrus.Fields{ - "method": "syncMerkleTree", - "treasury_pool": treasuryPoolRecord.Name, - }) - - // Assumption: There's only one process updating the merkle tree, but there - // are sufficient safeguards in this method and the store implementations such - // that we can recover. - treasuryPoolLock.Lock() - defer treasuryPoolLock.Unlock() - - // - // Part 1: Setup the DB-backed merkle tree, creating it if necessary - // - - log.Trace("setting up db-backed merkle tree") - - merkleTree, err := p.data.LoadExistingMerkleTree(ctx, treasuryPoolRecord.Name, false) - if err == merkletree.ErrMerkleTreeNotFound { - treasuryPoolAddressBytes, err := base58.Decode(treasuryPoolRecord.Address) - if err != nil { - log.WithError(err).Warn("failure decoding treasury pool address") - return err - } - - merkleTree, err = p.data.InitializeNewMerkleTree( - ctx, - treasuryPoolRecord.Name, - treasuryPoolRecord.MerkleTreeLevels, - []merkletree.Seed{ - splitter_token.MerkleTreePrefix, - treasuryPoolAddressBytes, - }, - false, - ) - if err != nil { - log.WithError(err).Warn("failure creating new cached merkle tree") - return err - } - } else if err != nil { - log.WithError(err).Warn("failure loading cached merkle tree") - return err - } - - // - // Part 2: Check if the merkle tree is already in sync - // - - log.Trace("checking whether merkle tree is already in sync") - - currentRootNode, err := merkleTree.GetCurrentRootNode(ctx) - if err != nil && err != merkletree.ErrRootNotFound { - log.WithError(err).Warn("failure getting current root node") - return err - } - - if currentRootNode != nil && currentRootNode.Hash.String() == treasuryPoolRecord.GetMostRecentRoot() { - log.Trace("merkle tree is already in sync") - return nil - } - - // - // Part 3: Find the fulfillment whose signature acts as the ending checkpoint for - // treasury payments. This ensures we create a reasonable and well-defined - // upper bound on processing treasury payments. - // - - log.Trace("looking for ending checkpoint") - - intentRecord, err := p.data.GetLatestSaveRecentRootIntentForTreasury(ctx, treasuryPoolRecord.Address) - if err == intent.ErrIntentNotFound { - // Nothing to do, since a recent root has never been saved yet - log.Trace("waiting for server to save the first recent root") - return nil - } else if err != nil { - log.WithError(err).Warn("failure getting intent record") - return err - } - - if intentRecord.State != intent.StateConfirmed { - log.Trace("last saved recent root intent isn't confirmed") - return errors.New("last saved recent root intent isn't confirmed") - } - - // Assumption: There's only one action and fulfillment for a save recent root intent. - checkpoint, err := p.data.GetAllFulfillmentsByAction(ctx, intentRecord.IntentId, 0) - if err != nil { - log.WithError(err).Warn("failure getting fulfillments for action") - return err - } - - txnRecord, err := p.getTransaction(ctx, *checkpoint[0].Signature) - if err != nil { - log.WithError(err).Warn("failure getting transaction for fulfillment") - return err - } - - endingSignature := *checkpoint[0].Signature - endingBlock := txnRecord.Slot - log = log.WithField("ending_signature", endingSignature) - log = log.WithField("ending_block", endingBlock) - log.Trace("found ending checkpoint") - - // - // Part 4: Find the fulfillment whose signature acts as the starting checkpoint - // for treasury payments. It will act as the exclusive lower bound for - // processing treasury payments. - // - - log.Trace("looking for starting checkpoint") - - // We're going to be operating on Solana blocks to determine order of transaction - // submission to the treasury pool. The scheduler will schedule transfer with commitment - // transactions in parallel, so simply observing the order in our intent, fulfillment or - // payment tables isn't sufficient. We'll need to figure out where we've left off by observing - // the state of the last leaf added to the merkle tree, which will allow us to infer a - // signature and Solana block to start from. - var startingBlock uint64 - var startingSignature string - leafNode, err := merkleTree.GetLastAddedLeafNode(ctx) - switch err { - case nil: - // Note this may not be a recent root in a treasury's history list. It's very well - // possible that we failed midway saving leaves to the DB, for example. - commitmentAddress := base58.Encode(leafNode.LeafValue) - log := log.WithField("last_leaf_value", commitmentAddress) - log.Trace("found the last leaf value") - - commitmentRecord, err := p.data.GetCommitmentByAddress(ctx, commitmentAddress) - if err != nil { - log.WithError(err).Warn("failure getting commitment record") - return err - } - - // Assumption: There's one transfer with commitment transaction per private transfer action - fulfillmentRecords, err := p.data.GetAllFulfillmentsByAction(ctx, commitmentRecord.Intent, commitmentRecord.ActionId) - if err != nil { - log.WithError(err).Warn("failure getting fulfillments from action") - return err - } - - var checkpoint *fulfillment.Record - for _, fulfillmentRecord := range fulfillmentRecords { - if fulfillmentRecord.State != fulfillment.StateConfirmed { - continue - } - - if fulfillmentRecord.FulfillmentType != fulfillment.TransferWithCommitment { - continue - } - - checkpoint = fulfillmentRecord - break - } - if checkpoint == nil { - log.Warn("cannot find fulfillment checkpoint") - return errors.New("cannot find fulfillment checkpoint") - } - - paymentRecord, err := p.data.GetPayment(ctx, *checkpoint.Signature, 2) - if err != nil { - log.WithError(err).Warn("failure getting payment record from fulfillment") - return err - } - - startingSignature = *checkpoint.Signature - startingBlock = paymentRecord.BlockId - log = log.WithField("starting_signature", startingSignature) - log = log.WithField("starting_block", startingBlock) - log.Trace("found starting checkpoint") - case merkletree.ErrLeafNotFound: - // Start from the very beginning (ie. Solana block 0), since the merkle - // tree is empty. - log.Trace("starting from block 0 and without a checkpoint because the merkle tree is empty") - default: - log.WithError(err).Warn("failure getting latest leaf node") - return err - } - - // - // Part 5: Safely add ordered treasury payments to the merkle tree - // - - log.Trace("processing ordered treasury payments to add to the merkle tree") - - // Get all payment records from our treasury pool. This assumes the treasury - // pool vault will only be used as a source for this kind of fulfillment. - var allTreasuryPayments []*payment.Record - var cursor query.Cursor - for { - endingBlockToQuery := endingBlock + 1 - startingBlockToQuery := startingBlock - if startingBlockToQuery > 0 { - startingBlockToQuery -= 1 - } - - paymentRecords, err := p.data.GetPaymentHistoryWithinBlockRange( - ctx, - treasuryPoolRecord.Vault, - startingBlockToQuery, - endingBlockToQuery, - query.WithFilter(query.Filter{Value: uint64(payment.PaymentType_Send), Valid: true}), - query.WithLimit(1000), - query.WithCursor(cursor), - ) - if err == payment.ErrNotFound { - break - } else if err != nil { - log.WithError(err).Warn("failure getting payment records") - return err - } - - allTreasuryPayments = append(allTreasuryPayments, paymentRecords...) - - cursor = query.ToCursor(paymentRecords[len(paymentRecords)-1].Id) - } - - semiSortedTreasuryPayments := payment.ByBlock(allTreasuryPayments) - sort.Sort(semiSortedTreasuryPayments) - - // Group payment records by their block. We cannot determine the order within - // a given block, yet. - var sortedBlocks []uint64 - treasuryPaymentsByBlock := make(map[uint64][]*payment.Record) - for _, treasuryPayment := range semiSortedTreasuryPayments { - solanaBlock := treasuryPayment.BlockId - if solanaBlock > endingBlock { - continue - } - - _, ok := treasuryPaymentsByBlock[solanaBlock] - if !ok { - sortedBlocks = append(sortedBlocks, solanaBlock) - } - - treasuryPaymentsByBlock[solanaBlock] = append(treasuryPaymentsByBlock[solanaBlock], treasuryPayment) - } - - // Order all treasury payment records - var foundStartingCheckpoint, foundEndingCheckpoint bool - var orderedPaymentsToAdd []*payment.Record - for _, solanaBlockId := range sortedBlocks { - log := log.WithField("solana_block", solanaBlockId) - log.Trace("processing solana block") - - // Break out of the loop if we've hit one form of the defined upper bound - if foundEndingCheckpoint || solanaBlockId > endingBlock { - break - } - - treasuryPaymentsOnBlock := treasuryPaymentsByBlock[solanaBlockId] - - // If there's more than one treasury payment on this block, or it's the - // block we've saved a recent root on, we need to determine the order using - // the blockchain block info. - // - // todo: We should get this metadata from our DB - if len(treasuryPaymentsOnBlock) > 1 || solanaBlockId == endingBlock { - log.Trace("deferring to blockhain for treasury payment ordering") - - orderedSignatures, err := p.data.GetBlockchainBlockSignatures(ctx, solanaBlockId) - if err != nil { - log.WithError(err).Warn("failure getting block from blockchain") - return err - } - - for _, sig := range orderedSignatures { - if sig == endingSignature { - foundEndingCheckpoint = true - break - } - - for _, treasuryPayment := range treasuryPaymentsOnBlock { - if treasuryPayment.TransactionId == sig { - log.WithField("signature", treasuryPayment.TransactionId).Trace("found treasury payment in block") - orderedPaymentsToAdd = append(orderedPaymentsToAdd, treasuryPayment) - } - } - } - } else { - orderedPaymentsToAdd = append(orderedPaymentsToAdd, treasuryPaymentsOnBlock[0]) - } - - // Anything on or before the starting signature we've checkpointed from has already - // been added to the merkle tree, so we need to remove them. This can only be done - // after processing the entire block because we don't know the order of intents - // within a block. - if !foundStartingCheckpoint && startingBlock > 0 { - for i, treasuryPayment := range orderedPaymentsToAdd { - if treasuryPayment.TransactionId == startingSignature { - foundStartingCheckpoint = true - orderedPaymentsToAdd = orderedPaymentsToAdd[i+1:] - break - } - } - - // We still haven't found the starting checkpoint, so clear the entire list - if !foundStartingCheckpoint { - orderedPaymentsToAdd = nil - } - } - } - - // Try adding the treasury payments to the merkle tree. There's no guarantee - // we have all the required information, because our feed of information from - // the blockchain can be delayed and out-of-order. If this fails, it's best to - // just wait and try again later after we've observed more of the blockchain's - // state. - err = p.safelyAddToMerkleTree(ctx, treasuryPoolRecord, merkleTree, orderedPaymentsToAdd) - if err == nil { - recordMerkleTreeSyncedEvent(ctx, treasuryPoolRecord.Name) - } - return err -} - -// This function expects paymentsToAdd[0] to be the first leaf after the last one saved -// and paymentsToAdd[len(paymentsToAdd)-1] to land on a recent root in the treasury's -// history list. This enables us to optimize the number of times we need to perform a -// simulation down to one. -func (p *service) safelyAddToMerkleTree(ctx context.Context, treasuryPoolRecord *treasury.Record, merkleTree *merkletree.MerkleTree, paymentsToAdd []*payment.Record) error { - log := p.log.WithFields(logrus.Fields{ - "method": "safelyAddToMerkleTree", - "treasury_pool": treasuryPoolRecord.Name, - }) - - log.Trace("attempting to add leaves to the merkle tree") - - if len(paymentsToAdd) == 0 { - log.Trace("no treasury payments to add to the merkle tree") - return errors.New("no treasury payments to add to the merkle tree") - } - - // For each treasury payment, get the leaf node value we'll add to the merkle tree. - // In this case, it's the commitment account's address. - // - // todo: This can be slow if done serially and can be optimized with parallelization - var leavesToAdd []merkletree.Leaf - for _, paymentRecord := range paymentsToAdd { - log := log.WithField("payment", paymentRecord.TransactionId) - log.Trace("processing treasury payment") - - commitmentRecord, err := p.getCommitmentFromPayment(ctx, paymentRecord) - if err != nil { - log.WithError(err).Warn("failure getting commitment for transaction") - return err - } - - commitmentAddressBytes, err := base58.Decode(commitmentRecord.Address) - if err != nil { - log.WithError(err).Warn("failure decoding commitment address") - return err - } - - log.WithField("leaf_value", commitmentRecord.Address).Trace("calculated leaf value") - leavesToAdd = append(leavesToAdd, commitmentAddressBytes) - } - - // Simulate the root hash by applying all leaves in advance to the merkle tree's - // local state - simulatedRoot, err := merkleTree.SimulateAddingLeaves(ctx, leavesToAdd) - if err != nil { - log.WithError(err).Warn("failure simulating root") - return err - } - log.WithField("simulated_root", simulatedRoot.String()).Trace("simulated root value") - - // Validate the simulated root against the expected observed treasury pool one. We - // only want to commit values to the DB when we're 100% we have a consistent state - // with the blockchain. - actualRecentRoot := simulatedRoot - expectedRecentRoot := treasuryPoolRecord.GetMostRecentRoot() - if expectedRecentRoot != actualRecentRoot.String() { - log.WithFields(logrus.Fields{ - "expected": expectedRecentRoot, - "actual": actualRecentRoot.String(), - }).Info("calculated an incorrect recent root") - return errors.New("calculated an incorrect recent root") - } - - log.Trace("merkle tree root simulation was successful") - - // Commit the leaves to the DB now that we've validated we can compute the - // same expected treasury pool most recent root. - for _, leaf := range leavesToAdd { - log := log.WithField("leaf_value", base58.Encode(leaf)) - log.Trace("persisting leaf to db") - err = merkleTree.AddLeaf(ctx, leaf) - if err == merkletree.ErrStaleMerkleTree { - // Should never happen if we're locking correctly, but refresh and try - // again later. - log.Warn("merkle tree is unexpectedly stale") - return err - } else if err != nil { - log.WithError(err).Warn("failure persisting leaf to db") - return err - } - } - - return nil -} - -// todo: This can likely be optimized with caching or better data modeling/querying -func (p *service) getCommitmentFromPayment(ctx context.Context, paymentRecord *payment.Record) (*commitment.Record, error) { - fulfillmentRecord, err := p.data.GetFulfillmentBySignature(ctx, paymentRecord.TransactionId) - if err != nil { - return nil, err - } - - return p.data.GetCommitmentByAction(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) -} - -func (p *service) getTransaction(ctx context.Context, signature string) (*transaction.Record, error) { - record, err := p.data.GetTransaction(ctx, signature) - if err == transaction.ErrNotFound { - return p.getTransactionFromBlockchain(ctx, signature) - } - return record, err -} - -func (p *service) getTransactionFromBlockchain(ctx context.Context, signature string) (*transaction.Record, error) { - stx, err := p.data.GetBlockchainTransaction(ctx, signature, solana.CommitmentFinalized) - if err == solana.ErrSignatureNotFound { - return nil, transaction.ErrNotFound - } - if err != nil { - return nil, err - } - - tx, err := transaction.FromConfirmedTransaction(stx) - if err != nil { - return nil, err - } - - return tx, nil -} diff --git a/pkg/code/async/treasury/merkle_tree_test.go b/pkg/code/async/treasury/merkle_tree_test.go deleted file mode 100644 index e32fabe5..00000000 --- a/pkg/code/async/treasury/merkle_tree_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package async_treasury - -import ( - "math/rand" - "strings" - "testing" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/intent" -) - -// todo: Add tests for syncMerkleTree for treasury payments that land on the -// same block, which requires a mocked Solana client that doesn't currently -// exist. - -func TestSyncMerkleTree(t *testing.T) { - env := setup(t, &testOverrides{}) - - // Nothing happens when there are no values to sync - require.NoError(t, env.worker.syncMerkleTree(env.ctx, env.treasuryPool)) - env.assertEmptyMerkleTree(t) - - // Empty merkle tree (no starting checkpoint, but an endpoint checkpoint exists) - allCommitmentRecords := env.simulateCommitments(t, 100, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateMostRecentRoot(t, intent.StateConfirmed, allCommitmentRecords) - require.NoError(t, env.worker.syncMerkleTree(env.ctx, env.treasuryPool)) - env.assertSyncedMerkleTree(t, allCommitmentRecords) - - // Merkle tree with some leaves (both starting and ending checkpoint exist) over - // a few iterations - for i := 0; i < 10; i++ { - newCommitmentRecords := env.simulateCommitments(t, rand.Intn(100)+1, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - env.simulateMostRecentRoot(t, intent.StatePending, newCommitmentRecords) - - // Intent isn't confirmed, so we error out - assert.Error(t, env.worker.syncMerkleTree(env.ctx, env.treasuryPool)) - - // Initial sync, which should yeild an updated merkle tree - env.simulateConfirmedIntent(t) - require.NoError(t, env.worker.syncMerkleTree(env.ctx, env.treasuryPool)) - allCommitmentRecords = append(allCommitmentRecords, newCommitmentRecords...) - env.assertSyncedMerkleTree(t, allCommitmentRecords) - - // Idempotency check where we shouldn't resync leaf values that have already - // been added to the merkle tree - require.NoError(t, env.worker.syncMerkleTree(env.ctx, env.treasuryPool)) - env.assertSyncedMerkleTree(t, allCommitmentRecords) - } -} - -func TestSafelyAddToMerkleTree(t *testing.T) { - env := setup(t, &testOverrides{}) - - commitmentRecords := env.simulateCommitments(t, 10, env.treasuryPool.GetMostRecentRoot(), commitment.StateReadyToOpen) - - paymentRecords, err := env.data.GetPaymentHistory(env.ctx, env.treasuryPool.Vault) - require.NoError(t, err) - require.Len(t, paymentRecords, 10) - - // No leaves to add - err = env.worker.safelyAddToMerkleTree(env.ctx, env.treasuryPool, env.merkleTree, nil) - assert.True(t, strings.Contains(err.Error(), "no treasury payments to add to the merkle tree")) - env.assertEmptyMerkleTree(t) - - // Treasury pool most recent root isn't available to simulate adding leaves - err = env.worker.safelyAddToMerkleTree(env.ctx, env.treasuryPool, env.merkleTree, paymentRecords) - assert.True(t, strings.Contains(err.Error(), "calculated an incorrect recent root")) - env.assertEmptyMerkleTree(t) - - env.simulateMostRecentRoot(t, intent.StateConfirmed, commitmentRecords) - - // Payment records are out of order and won't lead to a successful simulation - firstPaymentRecord := paymentRecords[0] - paymentRecords[0] = paymentRecords[len(paymentRecords)-1] - paymentRecords[len(paymentRecords)-1] = firstPaymentRecord - err = env.worker.safelyAddToMerkleTree(env.ctx, env.treasuryPool, env.merkleTree, paymentRecords) - assert.True(t, strings.Contains(err.Error(), "calculated an incorrect recent root")) - env.assertEmptyMerkleTree(t) - - paymentRecords, err = env.data.GetPaymentHistory(env.ctx, env.treasuryPool.Vault) - require.NoError(t, err) - require.Len(t, paymentRecords, 10) - - // Subset of apyment records are missing and won't lead to a successful simulation - paymentRecords = append(paymentRecords[:3], paymentRecords[7:]...) - err = env.worker.safelyAddToMerkleTree(env.ctx, env.treasuryPool, env.merkleTree, paymentRecords) - assert.True(t, strings.Contains(err.Error(), "calculated an incorrect recent root")) - env.assertEmptyMerkleTree(t) - - paymentRecords, err = env.data.GetPaymentHistory(env.ctx, env.treasuryPool.Vault) - require.NoError(t, err) - require.Len(t, paymentRecords, 10) - - // Payment records are in the right order and will successfully lead to a correct simulation - require.NoError(t, env.worker.safelyAddToMerkleTree(env.ctx, env.treasuryPool, env.merkleTree, paymentRecords)) - require.NoError(t, env.merkleTree.Refresh(env.ctx)) - for i, commitmentRecord := range commitmentRecords { - expectedLeafValue, err := base58.Decode(commitmentRecord.Address) - require.NoError(t, err) - - leafNode, err := env.merkleTree.GetLeafNodeByIndex(env.ctx, uint64(i)) - require.NoError(t, err) - assert.EqualValues(t, expectedLeafValue, leafNode.LeafValue) - } - - // Can't double add leaves - err = env.worker.safelyAddToMerkleTree(env.ctx, env.treasuryPool, env.merkleTree, paymentRecords) - assert.True(t, strings.Contains(err.Error(), "calculated an incorrect recent root")) -} diff --git a/pkg/code/async/treasury/metrics.go b/pkg/code/async/treasury/metrics.go deleted file mode 100644 index 626fbdf6..00000000 --- a/pkg/code/async/treasury/metrics.go +++ /dev/null @@ -1,84 +0,0 @@ -package async_treasury - -import ( - "context" - "time" - - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/metrics" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/treasury" -) - -const ( - treasuryFundCheckEventName = "TreasuryFundPollingCheck" - recentRootIntentCreatedEventName = "RecentRootIntentCreated" - merkleTreeSyncedEventName = "MerkleTreeSynced" -) - -func (p *service) metricsGaugeWorker(ctx context.Context) error { - delay := time.Second - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(delay): - start := time.Now() - - treasuryPoolRecords, err := p.data.GetAllTreasuryPoolsByState(ctx, treasury.TreasuryPoolStateAvailable) - if err != nil { - continue - } - - for _, treasuryPoolRecord := range treasuryPoolRecords { - total, used, err := estimateTreasuryPoolFundingLevels(ctx, p.data, treasuryPoolRecord) - if err != nil { - continue - } - recordTreasuryFundEvent(ctx, treasuryPoolRecord.Name, total, used) - } - - delay = time.Second - time.Since(start) - } - } -} - -func estimateTreasuryPoolFundingLevels(ctx context.Context, data code_data.Provider, record *treasury.Record) (total uint64, used uint64, err error) { - total, err = data.GetTotalAvailableTreasuryPoolFunds(ctx, record.Vault) - if err != nil { - return 0, 0, err - } - - used, err = data.GetUsedTreasuryPoolDeficitFromCommitments(ctx, record.Address) - if err != nil { - return 0, 0, err - } - - return total, used, nil -} - -func recordTreasuryFundEvent(ctx context.Context, name string, total, used uint64) { - var available uint64 - if used < total { - available = total - used - } - - metrics.RecordEvent(ctx, treasuryFundCheckEventName, map[string]interface{}{ - "treasury": name, - "available": kin.FromQuarks(available), - "used": kin.FromQuarks(used), - }) -} - -func recordRecentRootIntentCreatedEvent(ctx context.Context, treasuryPoolName string) { - metrics.RecordEvent(ctx, recentRootIntentCreatedEventName, map[string]interface{}{ - "treasury": treasuryPoolName, - }) -} - -func recordMerkleTreeSyncedEvent(ctx context.Context, treasuryPoolName string) { - metrics.RecordEvent(ctx, merkleTreeSyncedEventName, map[string]interface{}{ - "treasury": treasuryPoolName, - }) -} diff --git a/pkg/code/async/treasury/recent_root.go b/pkg/code/async/treasury/recent_root.go deleted file mode 100644 index 0d9b8f4a..00000000 --- a/pkg/code/async/treasury/recent_root.go +++ /dev/null @@ -1,388 +0,0 @@ -package async_treasury - -import ( - "context" - "database/sql" - "math" - "time" - - "github.com/mr-tron/base58" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/merkletree" - "github.com/code-payments/code-server/pkg/code/data/nonce" - "github.com/code-payments/code-server/pkg/code/data/payment" - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/code/transaction" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/cvm" -) - -// This method is expected to be extremely safe due to the implications of saving -// too many recent roots too fast. There's a high risk of breaking repayments. -// At the very least, we can only save a recent root when the previous one has been -// submitted and we've observed the updated treasury pool state. -// -// todo: We could also wait for proof initializations, but I think it's fine to ignore -// for now. It's highly unlikely we'll progress the treasury so far that the most recent -// root for the proof initialization won't be valid. We can always replace the transactions -// anyways. -func (p *service) maybeSaveRecentRoot(ctx context.Context, treasuryPoolRecord *treasury.Record) error { - log := p.log.WithFields(logrus.Fields{ - "method": "maybeSaveRecentRoot", - "treasury": treasuryPoolRecord.Name, - }) - - treasuryPoolLock.Lock() - defer treasuryPoolLock.Unlock() - - // - // Safety checks part 1: - // Ensure it's ok to proceed based on the state of the previous intent to save - // a recent root. - // - - // todo: This doesn't enable multiple bucketted treasuries being used concurrently - previousIntentRecord, err := p.data.GetLatestSaveRecentRootIntentForTreasury(ctx, treasuryPoolRecord.Address) - if err != nil && err != intent.ErrIntentNotFound { - log.WithError(err).Warn("failure getting previous intent record") - return err - } - - // Wait for the previous intent to complete before starting a new one - if previousIntentRecord != nil && previousIntentRecord.State != intent.StateConfirmed { - log.Trace("previous intent record isn't confirmed") - return p.openTreasuryAdvanceFloodGates(ctx, treasuryPoolRecord, previousIntentRecord) - } - - // - // Safety checks part 2: - // Ensure our view of the treasury pool state is up-to-date - // - - // The general treasury pool account state is not yet up-to-date - if previousIntentRecord != nil && previousIntentRecord.SaveRecentRootMetadata.PreviousMostRecentRoot == treasuryPoolRecord.GetMostRecentRoot() { - log.Trace("local treasury state hasn't been synced") - return nil - } - - merkleTree, err := p.data.LoadExistingMerkleTree(ctx, treasuryPoolRecord.Name, false) - if err != nil { - log.WithError(err).Warn("failure loding merkle tree") - return err - } - - currentRootNode, err := merkleTree.GetCurrentRootNode(ctx) - if err != nil && err != merkletree.ErrRootNotFound { - return err - } - - // The merkle tree pool is not yet up-to-date - if currentRootNode.Hash.String() != treasuryPoolRecord.GetMostRecentRoot() { - log.Trace("local merkle tree state hasn't been synced") - return nil - } - - // - // Safety checks part 3: - // Ensure it's ok to proceed based on whether minimum required progress is made - // - - numAdvancesCollected, err := p.data.GetFulfillmentCountByTypeStateAndAddressAsSource( - ctx, - fulfillment.TransferWithCommitment, - fulfillment.StateUnknown, - treasuryPoolRecord.Vault, - ) - if err != nil { - return err - } - - // This will catch withdrawal advances that bypass the collection state - anyNewFinalizedTreasuryAdvances, err := p.anyFinalizedTreasuryAdvancesAfterLastSaveRecentRoot(ctx, treasuryPoolRecord) - if err != nil { - log.WithError(err).Warn("failure checking for new finalized treasury advances since last save") - return err - } - - // We must collect or have played out at least one advance before we can even - // think about attempting to save a recent root. Otherwise, we risk saving the - // same one twice. - if numAdvancesCollected == 0 && !anyNewFinalizedTreasuryAdvances { - log.Trace("no treasury advances since last save") - return nil - } - - // - // Safety checks complete. Now we move on to hide in the crowd privacy checks. - // - - collectionTimeoutStartingPoint := treasuryPoolRecord.LastUpdatedAt // Brand new treasury - if previousIntentRecord != nil { - collectionTimeoutStartingPoint = previousIntentRecord.CreatedAt // Existing treasury with at least one recent root saved - } - - // Did we timeout collecting like-sized transactions? Doing this has a couple benefits: - // * Allows temporary privacy cheques to be cashed for unlocked accounts - // * Allows server to progress under low load - // This shouldn't be an issue at high volume anyways, and is a dead-simple heuristic - // to account for the above points. We can always add something more complex later. - if time.Since(collectionTimeoutStartingPoint) < p.conf.advanceCollectionTimeout.Get(ctx) { - minAdvancesRequired := getMinTransactionsForBucketedPrivacyLevel(p.conf.hideInCrowdPrivacyLevel.Get(ctx)) - - // Have we collected sufficient like-sized transactions? - if numAdvancesCollected < minAdvancesRequired { - log.Tracef("need to collect %d more treasury advances", minAdvancesRequired-numAdvancesCollected) - return nil - } - } else { - log.Trace("timed out collecting advances") - } - - // - // All checks have passed, so we can now move on to begin the process of - // saving the recent root. - // - - log.Tracef("initiating process to save recent root with %d advances collected", numAdvancesCollected) - - // Maintaining parity with how clients generate intent IDs - intentId, err := common.NewRandomAccount() - if err != nil { - return err - } - - intentRecord := &intent.Record{ - IntentId: intentId.PublicKey().ToBase58(), - IntentType: intent.SaveRecentRoot, - - InitiatorOwnerAccount: common.GetSubsidizer().PublicKey().ToBase58(), - - SaveRecentRootMetadata: &intent.SaveRecentRootMetadata{ - TreasuryPool: treasuryPoolRecord.Address, - PreviousMostRecentRoot: treasuryPoolRecord.GetMostRecentRoot(), - }, - - State: intent.StateUnknown, - } - - actionRecord := &action.Record{ - Intent: intentRecord.IntentId, - IntentType: intentRecord.IntentType, - - ActionId: 0, - ActionType: action.SaveRecentRoot, - - Source: treasuryPoolRecord.Vault, - - State: action.StatePending, - } - - selectedNonce, err := transaction.SelectAvailableNonce(ctx, p.data, nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeInternalServerProcess) - if err != nil { - log.WithError(err).Warn("failure selecting available nonce") - return err - } - defer selectedNonce.Unlock() - - txn, err := makeSaveRecentRootTransaction(selectedNonce, treasuryPoolRecord) - if err != nil { - log.WithError(err).Warn("failure creating transaction") - return err - } - - fulfillmentRecord := &fulfillment.Record{ - Intent: intentRecord.IntentId, - IntentType: intentRecord.IntentType, - - ActionId: actionRecord.ActionId, - ActionType: actionRecord.ActionType, - - FulfillmentType: fulfillment.SaveRecentRoot, - Data: txn.Marshal(), - Signature: pointer.String(base58.Encode(txn.Signature())), - - Nonce: pointer.String(selectedNonce.Account.PublicKey().ToBase58()), - Blockhash: pointer.String(base58.Encode(selectedNonce.Blockhash[:])), - - Source: treasuryPoolRecord.Vault, - - // IntentOrderingIndex unknown until intent record is saved - ActionOrderingIndex: 0, - FulfillmentOrderingIndex: 0, - - State: fulfillment.StateUnknown, - } - - // Create the intent in one DB transaction - err = p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { - err = p.data.SaveIntent(ctx, intentRecord) - if err != nil { - log.WithError(err).Warn("failure saving intent record") - return err - } - - err = p.data.PutAllActions(ctx, actionRecord) - if err != nil { - log.WithError(err).Warn("failure saving action record") - return err - } - - fulfillmentRecord.IntentOrderingIndex = intentRecord.Id // Unknown until intent record is saved - err = p.data.PutAllFulfillments(ctx, fulfillmentRecord) - if err != nil { - log.Warn("failure saving fulfillment record") - return err - } - - err = selectedNonce.MarkReservedWithSignature(ctx, *fulfillmentRecord.Signature) - if err != nil { - log.Warn("failure marking nonce reserved with signature") - return err - } - - // Intent is pending only after everything's been saved. - intentRecord.State = intent.StatePending - err = p.data.SaveIntent(ctx, intentRecord) - if err != nil { - log.WithError(err).Warn("failure marking intent as pending") - } - return err - }) - if err != nil { - return err - } - - recordRecentRootIntentCreatedEvent(ctx, treasuryPoolRecord.Name) - - return p.openTreasuryAdvanceFloodGates(ctx, treasuryPoolRecord, intentRecord) -} - -func (p *service) openTreasuryAdvanceFloodGates(ctx context.Context, treasuryPoolRecord *treasury.Record, saveRecentRootRecord *intent.Record) error { - log := p.log.WithFields(logrus.Fields{ - "method": "maybeSaveRecentRoot", - "treasury": treasuryPoolRecord.Name, - "intent_ordering_index": saveRecentRootRecord.Id, - }) - - limit := 10 * getMinTransactionsForBucketedPrivacyLevel(p.conf.hideInCrowdPrivacyLevel.Get(ctx)) - for i := 0; i < 10; i++ { // Bounded number of calls so we don't infinitely loop - count, err := p.data.ActivelyScheduleTreasuryAdvanceFulfillments( - ctx, - treasuryPoolRecord.Vault, - saveRecentRootRecord.Id, - int(limit), - ) - if err != nil { - log.WithError(err).Warn("failure marking treasury advances as actively scheduled") - return err - } - - if count == 0 { - return nil - } - - log.Tracef("%d treasury advances marked as actively scheduled", count) - - // Don't spam - time.Sleep(10 * time.Millisecond) - } - - log.Trace("stopped opening flood gates due to iteration limit") - return nil -} - -func makeSaveRecentRootTransaction(selectedNonce *transaction.SelectedNonce, record *treasury.Record) (solana.Transaction, error) { - vmAddressBytes, err := base58.Decode(record.Vm) - if err != nil { - return solana.Transaction{}, err - } - - treasuryAddressBytes, err := base58.Decode(record.Address) - if err != nil { - return solana.Transaction{}, err - } - - saveRecentRootInstruction := cvm.NewRelaySaveRecentRootInstruction( - &cvm.RelaySaveRecentRootInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vmAddressBytes, - Relay: treasuryAddressBytes, - }, - &cvm.RelaySaveRecentRootInstructionArgs{}, - ) - - // Always use a nonce for this type of transaction. It's way too risky without it, - // given the implications if we play this out too many times by accident. - txn, err := transaction.MakeNoncedTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, - saveRecentRootInstruction, - ) - if err != nil { - return solana.Transaction{}, err - } - - txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) - - return txn, nil -} - -func (p *service) anyFinalizedTreasuryAdvancesAfterLastSaveRecentRoot(ctx context.Context, treasuryPoolRecord *treasury.Record) (bool, error) { - var lowerBoundBlock uint64 - intentRecord, err := p.data.GetLatestSaveRecentRootIntentForTreasury(ctx, treasuryPoolRecord.Address) - switch err { - case nil: - // Still in progress - if intentRecord.State != intent.StateConfirmed { - return false, nil - } - - fulfillmentRecord, err := p.data.GetAllFulfillmentsByAction(ctx, intentRecord.IntentId, 0) - if err != nil { - return false, err - } - - txnRecord, err := p.getTransaction(ctx, *fulfillmentRecord[0].Signature) - if err != nil { - return false, err - } - - lowerBoundBlock = txnRecord.Slot - case intent.ErrIntentNotFound: - // New treasury pool without any recent roots saved - lowerBoundBlock = 0 - default: - return false, err - } - - paymentRecords, err := p.data.GetPaymentHistoryWithinBlockRange( - ctx, - treasuryPoolRecord.Vault, - lowerBoundBlock+1, - math.MaxInt64, - query.WithFilter(query.Filter{Value: uint64(payment.PaymentType_Send), Valid: true}), - query.WithLimit(1), - ) - if err == payment.ErrNotFound || len(paymentRecords) == 0 { - return false, nil - } else if err != nil { - return false, err - } - return true, nil -} - -// Assumption: To achieve 1 in X privacy with bucketed treasuries we only need -// to observe 2x minimum transactions. -// -// todo: Monitor these assumptions -// todo: We'll likely want to tune this per treasury -func getMinTransactionsForBucketedPrivacyLevel(level uint64) uint64 { - return 2 * 9 * level -} diff --git a/pkg/code/async/treasury/recent_root_test.go b/pkg/code/async/treasury/recent_root_test.go deleted file mode 100644 index 6f134172..00000000 --- a/pkg/code/async/treasury/recent_root_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package async_treasury - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/commitment" -) - -func TestMaybeSaveRecentRoot_HappyPath(t *testing.T) { - for _, allowSecondSave := range []bool{true, false} { - expectedMinAdvances := 180 - - env := setup(t, &testOverrides{ - hideInTheCrowdPrivacyLevel: 10, - }) - env.generateAvailableNonces(t, 2) - - require.EqualValues(t, expectedMinAdvances, getMinTransactionsForBucketedPrivacyLevel(10)) - - // No collected treasury advances - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentNotCreated(t) - - // Too few collected treasury advances - commitmentRecords := env.simulateCommitments(t, expectedMinAdvances/2, env.treasuryPool.GetMostRecentRoot(), commitment.StateUnknown) - - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentNotCreated(t) - - env.assertTreasuryAdvancesActiveSchedulingState(t, commitmentRecords, false) - - // Sufficient terasury advances collected - commitmentRecords = append( - commitmentRecords, - env.simulateCommitments(t, expectedMinAdvances/2, env.treasuryPool.GetMostRecentRoot(), commitment.StateUnknown)..., - ) - - if allowSecondSave { - // The most recent root value used shouldn't affect decision making. - // Notably, we won't include these commitments in the simulated worker - // flows so these commitments actually get applied for the second save. - env.simulateCommitments(t, expectedMinAdvances, env.treasuryPool.GetMostRecentRoot(), commitment.StateUnknown) - } - - // Recent root is now saved - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 1) - env.assertIntentCreated(t) - - env.assertTreasuryAdvancesActiveSchedulingState(t, commitmentRecords, true) - - // Simulate advances to the blockchain - env.simulateConfirmedAdvances(t, commitmentRecords) - - // Previous intent isn't confirmed - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 1) - - env.simulateConfirmedIntent(t) - - // Local treasury pool view hasn't been updated - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 1) - - nextRecentRoot := env.getNextRecentRoot(t, commitmentRecords) - env.simulateTreasuryPoolUpdated(t, commitmentRecords) - require.Equal(t, nextRecentRoot, env.treasuryPool.GetMostRecentRoot()) - - // Local merkle tree view hasn't been updated - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 1) - - env.simulateAddingLeaves(t, commitmentRecords) - - if allowSecondSave { - // Recent root can now be saved because sufficiently more advances have been collected - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 2) - env.assertIntentCreated(t) - } else { - // Recent root cannot be saved because advance collection hasn't progressed enough - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 1) - } - } -} - -func TestMaybeSaveRecentRoot_TimeoutWindow(t *testing.T) { - advanceCollectionTimeout := time.Second / 4 - - env := setup(t, &testOverrides{advanceCollectionTimeout: advanceCollectionTimeout}) - env.generateAvailableNonces(t, 2) - - commitmentRecords := env.simulateCommitments(t, 1, env.treasuryPool.GetMostRecentRoot(), commitment.StateUnknown) - - // Timeout not met for brand new treasury - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentNotCreated(t) - - time.Sleep(advanceCollectionTimeout) - - // Timeout met for brand new treasury - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 1) - env.assertIntentCreated(t) - - env.simulateConfirmedAdvances(t, commitmentRecords) - env.simulateConfirmedIntent(t) - env.simulateTreasuryPoolUpdated(t, commitmentRecords) - env.simulateAddingLeaves(t, commitmentRecords) - - commitmentRecords = env.simulateCommitments(t, 1, env.treasuryPool.GetMostRecentRoot(), commitment.StateUnknown) - - // Timeout not met since last saved recent root - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 1) - - time.Sleep(advanceCollectionTimeout) - - // Timeout met since last saved recent root - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 2) - env.assertIntentCreated(t) - - env.simulateConfirmedAdvances(t, commitmentRecords) - env.simulateConfirmedIntent(t) - env.simulateTreasuryPoolUpdated(t, commitmentRecords) - env.simulateAddingLeaves(t, commitmentRecords) - - time.Sleep(advanceCollectionTimeout) - - // Recent root isn't created if there are no advances and the timeout is met - require.NoError(t, env.worker.maybeSaveRecentRoot(env.ctx, env.treasuryPool)) - env.assertIntentCount(t, 2) -} diff --git a/pkg/code/async/treasury/service.go b/pkg/code/async/treasury/service.go deleted file mode 100644 index abf395b7..00000000 --- a/pkg/code/async/treasury/service.go +++ /dev/null @@ -1,58 +0,0 @@ -package async_treasury - -import ( - "context" - "time" - - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/async" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/treasury" -) - -type service struct { - log *logrus.Entry - conf *conf - data code_data.Provider -} - -func New(data code_data.Provider, configProvider ConfigProvider) async.Service { - return &service{ - log: logrus.StandardLogger().WithField("service", "treasury"), - conf: configProvider(), - data: data, - } -} - -func (p *service) Start(ctx context.Context, interval time.Duration) error { - // Setup workers to watch for updates to pools - for _, item := range []treasury.TreasuryPoolState{ - treasury.TreasuryPoolStateAvailable, - - // Below states have no executable logic - // treasury.TreasuryPoolStateUnknown, - // treasury.TreasuryPoolStateDeprecated, - } { - go func(state treasury.TreasuryPoolState) { - - err := p.worker(ctx, state, interval) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warnf("pool processing loop terminated unexpectedly for state %d", state) - } - - }(item) - } - - go func() { - err := p.metricsGaugeWorker(ctx) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("treasury metrics gauge loop terminated unexpectedly") - } - }() - - select { - case <-ctx.Done(): - return ctx.Err() - } -} diff --git a/pkg/code/async/treasury/testutil.go b/pkg/code/async/treasury/testutil.go deleted file mode 100644 index 3e292d75..00000000 --- a/pkg/code/async/treasury/testutil.go +++ /dev/null @@ -1,462 +0,0 @@ -package async_treasury - -import ( - "context" - "crypto/ed25519" - "encoding/hex" - "fmt" - "math/rand" - "testing" - "time" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/merkletree" - "github.com/code-payments/code-server/pkg/code/data/nonce" - "github.com/code-payments/code-server/pkg/code/data/payment" - "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/code/data/vault" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/solana" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" - "github.com/code-payments/code-server/pkg/solana/system" - "github.com/code-payments/code-server/pkg/testutil" -) - -type testEnv struct { - ctx context.Context - data code_data.Provider - treasuryPool *treasury.Record - merkleTree *merkletree.MerkleTree - worker *service - subsidizer *common.Account - nextBlock uint64 -} - -func setup(t *testing.T, testOverrides *testOverrides) *testEnv { - ctx := context.Background() - - db := code_data.NewTestDataProvider() - - subsidizer := testutil.SetupRandomSubsidizer(t, db) - - treasuryPoolAddress := testutil.NewRandomAccount(t) - treasuryPool := &treasury.Record{ - Vm: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - Name: "test-pool", - - Address: treasuryPoolAddress.PublicKey().ToBase58(), - Bump: 123, - - Vault: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - VaultBump: 100, - - Authority: subsidizer.PublicKey().ToBase58(), - - MerkleTreeLevels: 63, - - CurrentIndex: 1, - HistoryListSize: 5, - - SolanaBlock: 1, - - State: treasury.TreasuryPoolStateAvailable, - } - - merkleTree, err := db.InitializeNewMerkleTree( - ctx, - treasuryPool.Name, - treasuryPool.MerkleTreeLevels, - []merkletree.Seed{ - splitter_token.MerkleTreePrefix, - treasuryPoolAddress.PublicKey().ToBytes(), - }, - false, - ) - require.NoError(t, err) - - rootNode, err := merkleTree.GetCurrentRootNode(ctx) - require.NoError(t, err) - for i := 0; i < int(treasuryPool.HistoryListSize); i++ { - treasuryPool.HistoryList = append(treasuryPool.HistoryList, hex.EncodeToString(rootNode.Hash)) - } - require.NoError(t, db.SaveTreasuryPool(ctx, treasuryPool)) - - return &testEnv{ - ctx: ctx, - data: db, - treasuryPool: treasuryPool, - merkleTree: merkleTree, - worker: New(db, withManualTestOverrides(testOverrides)).(*service), - subsidizer: subsidizer, - nextBlock: 1, - } -} - -func (e *testEnv) simulateMostRecentRoot(t *testing.T, intentState intent.State, commitmentRecords []*commitment.Record) { - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.SaveRecentRoot, - InitiatorOwnerAccount: e.subsidizer.PublicKey().ToBase58(), - SaveRecentRootMetadata: &intent.SaveRecentRootMetadata{ - TreasuryPool: e.treasuryPool.Address, - PreviousMostRecentRoot: e.treasuryPool.GetMostRecentRoot(), - }, - State: intentState, - } - require.NoError(t, e.data.SaveIntent(e.ctx, intentRecord)) - - fulfillmentRecord := &fulfillment.Record{ - Intent: intentRecord.IntentId, - IntentType: intent.SaveRecentRoot, - - ActionId: 0, - ActionType: action.SaveRecentRoot, - - FulfillmentType: fulfillment.SaveRecentRoot, - Data: []byte("data"), - Signature: pointer.String(fmt.Sprintf("sig%d", rand.Uint64())), - - Nonce: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - Blockhash: pointer.String("bh"), - - Source: e.treasuryPool.Vault, - - State: fulfillment.StateUnknown, - } - require.NoError(t, e.data.PutAllFulfillments(e.ctx, fulfillmentRecord)) - - txnRecord := &transaction.Record{ - Signature: *fulfillmentRecord.Signature, - Slot: e.getNextBlock(), - ConfirmationState: transaction.ConfirmationFinalized, - } - require.NoError(t, e.data.SaveTransaction(e.ctx, txnRecord)) - - e.simulateTreasuryPoolUpdated(t, commitmentRecords) -} - -func (e *testEnv) simulateTreasuryPoolUpdated(t *testing.T, commitmentRecords []*commitment.Record) { - var leavesToSimulate []merkletree.Leaf - for _, commitmentRecord := range commitmentRecords { - addressBytes, err := base58.Decode(commitmentRecord.Address) - require.NoError(t, err) - - leavesToSimulate = append(leavesToSimulate, addressBytes) - } - hash, err := e.merkleTree.SimulateAddingLeaves(e.ctx, leavesToSimulate) - require.NoError(t, err) - - e.treasuryPool.CurrentIndex = (e.treasuryPool.CurrentIndex + 1) % e.treasuryPool.HistoryListSize - e.treasuryPool.HistoryList[e.treasuryPool.CurrentIndex] = hex.EncodeToString(hash) - e.treasuryPool.SolanaBlock = e.getNextBlock() - require.NoError(t, e.data.SaveTreasuryPool(e.ctx, e.treasuryPool)) -} - -func (e *testEnv) simulateAddingLeaves(t *testing.T, commitmentRecords []*commitment.Record) { - for _, commitmentRecord := range commitmentRecords { - addressBytes, err := base58.Decode(commitmentRecord.Address) - require.NoError(t, err) - - require.NoError(t, e.merkleTree.AddLeaf(e.ctx, addressBytes)) - } -} - -func (e *testEnv) simulateCommitments(t *testing.T, count int, recentRoot string, state commitment.State) []*commitment.Record { - var commitmentRecords []*commitment.Record - for i := 0; i < count; i++ { - commitmentRecord := &commitment.Record{ - Address: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - VaultAddress: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - Pool: e.treasuryPool.Address, - RecentRoot: recentRoot, - - Transcript: "transcript", - Destination: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Amount: kin.ToQuarks(1), - - Intent: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - ActionId: rand.Uint32(), - - Owner: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - State: state, - } - require.NoError(t, e.data.SaveCommitment(e.ctx, commitmentRecord)) - commitmentRecords = append(commitmentRecords, commitmentRecord) - - fulfillmentRecord := &fulfillment.Record{ - Intent: commitmentRecord.Intent, - IntentType: intent.SendPrivatePayment, - - ActionId: commitmentRecord.ActionId, - ActionType: action.PrivateTransfer, - - FulfillmentType: fulfillment.TransferWithCommitment, - Data: []byte("data"), - Signature: pointer.String(fmt.Sprintf("sig%d", rand.Uint64())), - - Nonce: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - Blockhash: pointer.String("bh"), - - Source: e.treasuryPool.Vault, - Destination: &commitmentRecord.Destination, - - DisableActiveScheduling: true, - - State: fulfillment.StateUnknown, - } - require.NoError(t, e.data.PutAllFulfillments(e.ctx, fulfillmentRecord)) - - if state >= commitment.StateReadyToOpen { - fulfillmentRecord.DisableActiveScheduling = false - fulfillmentRecord.State = fulfillment.StateConfirmed - require.NoError(t, e.data.UpdateFulfillment(e.ctx, fulfillmentRecord)) - - paymentRecord := &payment.Record{ - Source: fulfillmentRecord.Source, - Destination: *fulfillmentRecord.Destination, - BlockId: e.getNextBlock(), - TransactionId: *fulfillmentRecord.Signature, - TransactionIndex: 2, - } - require.NoError(t, e.data.CreatePayment(e.ctx, paymentRecord)) - } - } - return commitmentRecords -} - -func (e *testEnv) simulateConfirmedAdvances(t *testing.T, commitmentRecords []*commitment.Record) { - for _, commitmentRecord := range commitmentRecords { - if commitmentRecord.State != commitment.StateUnknown { - continue - } - - commitmentRecord.State = commitment.StateReadyToOpen - require.NoError(t, e.data.SaveCommitment(e.ctx, commitmentRecord)) - - fulfillmentRecords, err := e.data.GetAllFulfillmentsByTypeAndAction(e.ctx, fulfillment.TransferWithCommitment, commitmentRecord.Intent, commitmentRecord.ActionId) - require.NoError(t, err) - require.Len(t, fulfillmentRecords, 1) - fulfillmentRecord := fulfillmentRecords[0] - - fulfillmentRecord.State = fulfillment.StateConfirmed - require.NoError(t, e.data.UpdateFulfillment(e.ctx, fulfillmentRecord)) - - paymentRecord := &payment.Record{ - Source: fulfillmentRecord.Source, - Destination: *fulfillmentRecord.Destination, - BlockId: e.getNextBlock(), - TransactionId: *fulfillmentRecord.Signature, - TransactionIndex: 2, - } - require.NoError(t, e.data.CreatePayment(e.ctx, paymentRecord)) - } -} - -func (e *testEnv) simulateConfirmedIntent(t *testing.T) { - intentRecord, err := e.data.GetLatestIntentByInitiatorAndType(e.ctx, intent.SaveRecentRoot, e.subsidizer.PublicKey().ToBase58()) - require.NoError(t, err) - require.Equal(t, intentRecord.IntentType, intent.SaveRecentRoot) - require.Equal(t, intent.StatePending, intentRecord.State) - - intentRecord.State = intent.StateConfirmed - require.NoError(t, e.data.SaveIntent(e.ctx, intentRecord)) - - fulfillmentRecords, err := e.data.GetAllFulfillmentsByAction(e.ctx, intentRecord.IntentId, 0) - require.NoError(t, err) - require.Len(t, fulfillmentRecords, 1) - fulfillmentRecord := fulfillmentRecords[0] - - txnRecord := transaction.Record{ - Signature: *fulfillmentRecord.Signature, - Slot: e.getNextBlock(), - } - require.NoError(t, e.data.SaveTransaction(e.ctx, &txnRecord)) -} - -func (e *testEnv) getNextRecentRoot(t *testing.T, commitmentRecords []*commitment.Record) string { - var leavesToAdd []merkletree.Leaf - for _, commitmentRecord := range commitmentRecords { - addressBytes, err := base58.Decode(commitmentRecord.Address) - require.NoError(t, err) - - leavesToAdd = append(leavesToAdd, addressBytes) - } - - hash, err := e.merkleTree.SimulateAddingLeaves(e.ctx, leavesToAdd) - require.NoError(t, err) - - return hex.EncodeToString(hash) -} - -func (e *testEnv) assertEmptyMerkleTree(t *testing.T) { - require.NoError(t, e.merkleTree.Refresh(e.ctx)) - - _, err := e.merkleTree.GetLeafNodeByIndex(e.ctx, 0) - assert.Equal(t, merkletree.ErrLeafNotFound, err) -} - -func (e *testEnv) assertIntentNotCreated(t *testing.T) { - _, err := e.data.GetLatestIntentByInitiatorAndType(e.ctx, intent.SaveRecentRoot, e.subsidizer.PublicKey().ToBase58()) - assert.Equal(t, intent.ErrIntentNotFound, err) -} - -func (e *testEnv) assertIntentCount(t *testing.T, expected int) { - actual, err := e.data.GetAllIntentsByOwner(e.ctx, e.subsidizer.PublicKey().ToBase58()) - require.NoError(t, err) - assert.EqualValues(t, expected, len(actual)) -} - -func (e *testEnv) assertIntentCreated(t *testing.T) { - intentRecord, err := e.data.GetLatestIntentByInitiatorAndType(e.ctx, intent.SaveRecentRoot, e.subsidizer.PublicKey().ToBase58()) - require.NoError(t, err) - require.Equal(t, intentRecord.IntentType, intent.SaveRecentRoot) - assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), intentRecord.InitiatorOwnerAccount) - assert.Nil(t, intentRecord.InitiatorPhoneNumber) - assert.Equal(t, intent.StatePending, intentRecord.State) - require.NotNil(t, intentRecord.SaveRecentRootMetadata) - assert.Equal(t, e.treasuryPool.Address, intentRecord.SaveRecentRootMetadata.TreasuryPool) - assert.Equal(t, e.treasuryPool.GetMostRecentRoot(), intentRecord.SaveRecentRootMetadata.PreviousMostRecentRoot) - - actionRecords, err := e.data.GetAllActionsByIntent(e.ctx, intentRecord.IntentId) - require.NoError(t, err) - require.Len(t, actionRecords, 1) - actionRecord := actionRecords[0] - assert.Equal(t, intentRecord.IntentId, actionRecord.Intent) - assert.Equal(t, intent.SaveRecentRoot, actionRecord.IntentType) - assert.EqualValues(t, 0, actionRecord.ActionId) - assert.Equal(t, action.SaveRecentRoot, actionRecord.ActionType) - assert.Equal(t, e.treasuryPool.Vault, actionRecord.Source) - assert.Nil(t, actionRecord.Destination) - assert.Nil(t, actionRecord.Quantity) - assert.Nil(t, actionRecord.InitiatorPhoneNumber) - assert.Equal(t, action.StatePending, actionRecord.State) - - fulfillmentRecords, err := e.data.GetAllFulfillmentsByIntent(e.ctx, intentRecord.IntentId) - require.NoError(t, err) - require.Len(t, fulfillmentRecords, 1) - fulfillmentRecord := fulfillmentRecords[0] - assert.Equal(t, fulfillmentRecord.Intent, intentRecord.IntentId) - assert.Equal(t, intent.SaveRecentRoot, fulfillmentRecord.IntentType) - assert.EqualValues(t, 0, fulfillmentRecord.ActionId) - assert.Equal(t, action.SaveRecentRoot, fulfillmentRecord.ActionType) - assert.Equal(t, fulfillment.SaveRecentRoot, fulfillmentRecord.FulfillmentType) - assert.Equal(t, e.treasuryPool.Vault, fulfillmentRecord.Source) - assert.Nil(t, fulfillmentRecord.Destination) - assert.Equal(t, intentRecord.Id, fulfillmentRecord.IntentOrderingIndex) - assert.EqualValues(t, 0, fulfillmentRecord.ActionOrderingIndex) - assert.EqualValues(t, 0, fulfillmentRecord.FulfillmentOrderingIndex) - assert.False(t, fulfillmentRecord.DisableActiveScheduling) - assert.Nil(t, fulfillmentRecord.InitiatorPhoneNumber) - assert.Equal(t, fulfillment.StateUnknown, fulfillmentRecord.State) - - var txn solana.Transaction - require.NoError(t, txn.Unmarshal(fulfillmentRecord.Data)) - require.Len(t, txn.Message.Instructions, 2) - - assert.Equal(t, *fulfillmentRecord.Blockhash, base58.Encode(txn.Message.RecentBlockhash[:])) - - expectedSignature := ed25519.Sign(e.subsidizer.PrivateKey().ToBytes(), txn.Message.Marshal()) - assert.Equal(t, base58.Encode(expectedSignature), *fulfillmentRecord.Signature) - assert.EqualValues(t, txn.Signatures[0][:], expectedSignature) - - advanceNonceIxn, err := system.DecompileAdvanceNonce(txn.Message, 0) - require.NoError(t, err) - - assert.Equal(t, *fulfillmentRecord.Nonce, base58.Encode(advanceNonceIxn.Nonce)) - assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), base58.Encode(advanceNonceIxn.Authority)) - - // todo: Ability to decompile CVM save recent root ixn (legacy code in comments) - /*saveRecentRootIxnArgs, saveRecentRootIxnAccounts, err := splitter_token.SaveRecentRootInstructionFromLegacyInstruction(txn, 1) - require.NoError(t, err) - assert.Equal(t, e.treasuryPool.Bump, saveRecentRootIxnArgs.PoolBump) - assert.Equal(t, e.treasuryPool.Address, base58.Encode(saveRecentRootIxnAccounts.Pool)) - assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), base58.Encode(saveRecentRootIxnAccounts.Authority)) - assert.Equal(t, e.subsidizer.PublicKey().ToBase58(), base58.Encode(saveRecentRootIxnAccounts.Payer))*/ - - nonceRecord, err := e.data.GetNonce(e.ctx, *fulfillmentRecord.Nonce) - require.NoError(t, err) - assert.Equal(t, nonce.StateReserved, nonceRecord.State) - assert.Equal(t, *fulfillmentRecord.Signature, nonceRecord.Signature) - assert.Equal(t, nonceRecord.Blockhash, *fulfillmentRecord.Blockhash) -} - -func (e *testEnv) assertSyncedMerkleTree(t *testing.T, commitmentRecords []*commitment.Record) { - require.NoError(t, e.merkleTree.Refresh(e.ctx)) - - latestLeafNode, err := e.merkleTree.GetLastAddedLeafNode(e.ctx) - require.NoError(t, err) - require.EqualValues(t, latestLeafNode.Index, len(commitmentRecords)-1) - - for i, commitmentRecord := range commitmentRecords { - leafNode, err := e.merkleTree.GetLeafNodeByIndex(e.ctx, uint64(i)) - require.NoError(t, err) - assert.Equal(t, commitmentRecord.Address, base58.Encode(leafNode.LeafValue)) - } -} - -func (e *testEnv) assertTreasuryAdvancesActiveSchedulingState(t *testing.T, commitmentRecords []*commitment.Record, expected bool) { - for _, commitmentRecord := range commitmentRecords { - if commitmentRecord.State != commitment.StateUnknown { - continue - } - - fulfillmentRecords, err := e.data.GetAllFulfillmentsByTypeAndAction(e.ctx, fulfillment.TransferWithCommitment, commitmentRecord.Intent, commitmentRecord.ActionId) - require.NoError(t, err) - require.Len(t, fulfillmentRecords, 1) - fulfillmentRecord := fulfillmentRecords[0] - assert.Equal(t, fulfillmentRecord.DisableActiveScheduling, !expected) - } -} - -func (e *testEnv) generateAvailableNonce(t *testing.T) *nonce.Record { - nonceAccount := testutil.NewRandomAccount(t) - - var bh solana.Blockhash - rand.Read(bh[:]) - - nonceKey := &vault.Record{ - PublicKey: nonceAccount.PublicKey().ToBase58(), - PrivateKey: nonceAccount.PrivateKey().ToBase58(), - State: vault.StateAvailable, - CreatedAt: time.Now(), - } - nonceRecord := &nonce.Record{ - Address: nonceAccount.PublicKey().ToBase58(), - Authority: e.subsidizer.PublicKey().ToBase58(), - Blockhash: base58.Encode(bh[:]), - Environment: nonce.EnvironmentSolana, - EnvironmentInstance: nonce.EnvironmentInstanceSolanaMainnet, - Purpose: nonce.PurposeInternalServerProcess, - State: nonce.StateAvailable, - } - require.NoError(t, e.data.SaveKey(e.ctx, nonceKey)) - require.NoError(t, e.data.SaveNonce(e.ctx, nonceRecord)) - return nonceRecord -} - -func (e *testEnv) generateAvailableNonces(t *testing.T, count int) []*nonce.Record { - var nonces []*nonce.Record - for i := 0; i < count; i++ { - nonces = append(nonces, e.generateAvailableNonce(t)) - } - return nonces -} - -func (e *testEnv) getNextBlock() uint64 { - e.nextBlock += 1 - return e.nextBlock -} diff --git a/pkg/code/async/treasury/worker.go b/pkg/code/async/treasury/worker.go deleted file mode 100644 index 1ef8ca0b..00000000 --- a/pkg/code/async/treasury/worker.go +++ /dev/null @@ -1,127 +0,0 @@ -package async_treasury - -import ( - "context" - "sync" - "time" - - "github.com/newrelic/go-agent/v3/newrelic" - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/retry" - "github.com/code-payments/code-server/pkg/solana/cvm" -) - -const ( - maxRecordBatchSize = 10 -) - -var ( - // todo: distributed lock - treasuryPoolLock sync.Mutex -) - -func (p *service) worker(serviceCtx context.Context, state treasury.TreasuryPoolState, interval time.Duration) error { - delay := interval - var cursor query.Cursor - - err := retry.Loop( - func() (err error) { - time.Sleep(delay) - - nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) - m := nr.StartTransaction("async__treasury_pool_service__handle_" + state.String()) - defer m.End() - tracedCtx := newrelic.NewContext(serviceCtx, m) - - // Get a batch of records in similar state - items, err := p.data.GetAllTreasuryPoolsByState( - tracedCtx, - state, - query.WithCursor(cursor), - query.WithDirection(query.Ascending), - query.WithLimit(maxRecordBatchSize), - ) - if err != nil && err != treasury.ErrTreasuryPoolNotFound { - cursor = query.EmptyCursor - return err - } - - // Process the batch of accounts in parallel - var wg sync.WaitGroup - for _, item := range items { - wg.Add(1) - go func(record *treasury.Record) { - defer wg.Done() - - err := p.handle(tracedCtx, record) - if err != nil { - m.NoticeError(err) - } - }(item) - } - wg.Wait() - - // Update cursor to point to the next set of pool - if len(items) > 0 { - cursor = query.ToCursor(items[len(items)-1].Id) - } else { - cursor = query.EmptyCursor - } - - return nil - }, - retry.NonRetriableErrors(context.Canceled), - ) - - return err -} - -func (p *service) handle(ctx context.Context, record *treasury.Record) error { - switch record.State { - case treasury.TreasuryPoolStateAvailable: - return p.handleAvailable(ctx, record) - default: - return nil - } -} - -func (p *service) handleAvailable(ctx context.Context, record *treasury.Record) error { - err := p.updateAccountState(ctx, record) - if err != nil && err != treasury.ErrStaleTreasuryPoolState { - return err - } - - err = p.syncMerkleTree(ctx, record) - if err != nil { - return err - } - - // Runs last, since we have an expectation that our state is up-to-date before - // saving a new recent root. - return p.maybeSaveRecentRoot(ctx, record) -} - -func (p *service) updateAccountState(ctx context.Context, record *treasury.Record) error { - // todo: Use a smarter block. Perhaps from the last finalized payment? - data, solanaBlock, err := p.data.GetBlockchainAccountDataAfterBlock(ctx, record.Address, record.SolanaBlock) - if err != nil { - return errors.Wrap(err, "error querying latest account data from blockchain") - } - - var unmarshalled cvm.RelayAccount - err = unmarshalled.Unmarshal(data) - if err != nil { - return errors.Wrap(err, "error unmarshalling account data") - } - - err = record.Update(&unmarshalled, solanaBlock) - if err != nil { - return err - } - - return p.data.SaveTreasuryPool(ctx, record) -} diff --git a/pkg/code/async/user/service.go b/pkg/code/async/user/service.go deleted file mode 100644 index 9230704c..00000000 --- a/pkg/code/async/user/service.go +++ /dev/null @@ -1,54 +0,0 @@ -package async_user - -import ( - "context" - "time" - - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/async" - code_data "github.com/code-payments/code-server/pkg/code/data" - push_lib "github.com/code-payments/code-server/pkg/push" - "github.com/code-payments/code-server/pkg/sync" - "github.com/code-payments/code-server/pkg/twitter" -) - -type service struct { - log *logrus.Entry - data code_data.Provider - pusher push_lib.Provider - twitterClient *twitter.Client - userLocks *sync.StripedLock -} - -func New(data code_data.Provider, pusher push_lib.Provider, twitterClient *twitter.Client) async.Service { - return &service{ - log: logrus.StandardLogger().WithField("service", "user"), - data: data, - pusher: pusher, - twitterClient: twitterClient, - userLocks: sync.NewStripedLock(1024), - } -} - -// todo: split out interval for each worker -func (p *service) Start(ctx context.Context, interval time.Duration) error { - go func() { - err := p.twitterRegistrationWorker(ctx, interval) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("twitter registration processing loop terminated unexpectedly") - } - }() - - go func() { - err := p.twitterUserInfoUpdateWorker(ctx, interval) - if err != nil && err != context.Canceled { - p.log.WithError(err).Warn("twitter user info processing loop terminated unexpectedly") - } - }() - - select { - case <-ctx.Done(): - return ctx.Err() - } -} diff --git a/pkg/code/async/user/twitter.go b/pkg/code/async/user/twitter.go deleted file mode 100644 index eb1b6f5d..00000000 --- a/pkg/code/async/user/twitter.go +++ /dev/null @@ -1,409 +0,0 @@ -package async_user - -import ( - "context" - "crypto/ed25519" - "database/sql" - "fmt" - "strings" - "time" - - "github.com/google/uuid" - "github.com/mr-tron/base58" - "github.com/newrelic/go-agent/v3/newrelic" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - userpb "github.com/code-payments/code-protobuf-api/generated/go/user/v1" - - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/twitter" - push_util "github.com/code-payments/code-server/pkg/code/push" - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/retry" - twitter_lib "github.com/code-payments/code-server/pkg/twitter" -) - -const ( - tipCardRegistrationPrefix = "CodeAccount" - maxTweetSearchResults = 100 // maximum allowed -) - -var ( - errTwitterInvalidRegistrationValue = errors.New("twitter registration value is invalid") - errTwitterRegistrationNotFound = errors.New("twitter registration not found") -) - -func (p *service) twitterRegistrationWorker(serviceCtx context.Context, interval time.Duration) error { - log := p.log.WithField("method", "twitterRegistrationWorker") - - delay := interval - - err := retry.Loop( - func() (err error) { - time.Sleep(delay) - - nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) - m := nr.StartTransaction("async__user_service__handle_twitter_registration") - defer m.End() - tracedCtx := newrelic.NewContext(serviceCtx, m) - - err = p.processNewTwitterRegistrations(tracedCtx) - if err != nil { - m.NoticeError(err) - log.WithError(err).Warn("failure processing new twitter registrations") - } - return err - }, - retry.NonRetriableErrors(context.Canceled), - ) - - return err -} - -func (p *service) twitterUserInfoUpdateWorker(serviceCtx context.Context, interval time.Duration) error { - log := p.log.WithField("method", "twitterUserInfoUpdateWorker") - - delay := interval - - err := retry.Loop( - func() (err error) { - time.Sleep(delay) - - nr := serviceCtx.Value(metrics.NewRelicContextKey).(*newrelic.Application) - m := nr.StartTransaction("async__user_service__handle_twitter_user_info_update") - defer m.End() - tracedCtx := newrelic.NewContext(serviceCtx, m) - - // todo: configurable parameters - records, err := p.data.GetStaleTwitterUsers(tracedCtx, 7*24*time.Hour, 32) - if err == twitter.ErrUserNotFound { - return nil - } else if err != nil { - m.NoticeError(err) - log.WithError(err).Warn("failure getting stale twitter users") - return err - } - - for _, record := range records { - err := p.refreshTwitterUserInfo(tracedCtx, record.Username) - if err != nil { - m.NoticeError(err) - log.WithError(err).Warn("failure refreshing twitter user info") - return err - } - } - - return nil - }, - retry.NonRetriableErrors(context.Canceled), - ) - - return err -} - -func (p *service) processNewTwitterRegistrations(ctx context.Context) error { - log := p.log.WithField("method", "processNewTwitterRegistrations") - - tweets, err := p.findNewRegistrationTweets(ctx) - if err != nil { - return errors.Wrap(err, "error finding new registration tweets") - } - - for _, tweet := range tweets { - if tweet.AdditionalMetadata.Author == nil { - return errors.Errorf("author missing in tweet %s", tweet.ID) - } - - log := log.WithFields(logrus.Fields{ - "tweet": tweet.ID, - "username": tweet.AdditionalMetadata.Author, - }) - - // Attempt to find a verified tip account from the registration tweet - tipAccount, registrationNonce, err := p.findVerifiedTipAccountRegisteredInTweet(ctx, tweet) - switch err { - case nil: - case errTwitterInvalidRegistrationValue, errTwitterRegistrationNotFound: - continue - default: - return errors.Wrapf(err, "unexpected error processing tweet %s", tweet.ID) - } - - // Save the updated tipping information - err = p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { - err = p.data.MarkTwitterNonceAsUsed(ctx, tweet.ID, *registrationNonce) - if err != nil { - return err - } - - err = p.updateCachedTwitterUser(ctx, tweet.AdditionalMetadata.Author, tipAccount) - if err != nil { - return err - } - - return p.data.MarkTweetAsProcessed(ctx, tweet.ID) - }) - - switch err { - case nil: - // todo: all of these success handlers are fire and forget best-effort delivery - - go func() { - err := push_util.SendTwitterAccountConnectedPushNotification(ctx, p.data, p.pusher, tipAccount) - if err != nil { - log.WithError(err).Warn("failure sending success push") - } - }() - - go func() { - err := p.sendRegistrationSuccessReply(ctx, tweet.ID, tweet.AdditionalMetadata.Author.Username) - if err != nil { - log.WithError(err).Warn("failure sending success reply") - } - }() - case twitter.ErrDuplicateTipAddress: - err = p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { - err = p.data.MarkTwitterNonceAsUsed(ctx, tweet.ID, *registrationNonce) - if err != nil { - return err - } - - return p.data.MarkTweetAsProcessed(ctx, tweet.ID) - }) - if err != nil { - return errors.Wrap(err, "error saving tweet with duplicate tip address metadata") - } - return nil - case twitter.ErrDuplicateNonce: - err = p.data.MarkTweetAsProcessed(ctx, tweet.ID) - if err != nil { - return errors.Wrap(err, "error marking tweet with duplicate nonce as processed") - } - return nil - default: - return errors.Wrap(err, "error saving new registration") - } - } - - return nil -} - -func (p *service) refreshTwitterUserInfo(ctx context.Context, username string) error { - user, err := p.twitterClient.GetUserByUsername(ctx, username) - if err != nil { - if strings.Contains(strings.ToLower(err.Error()), "could not find user with username") || strings.Contains(strings.ToLower(err.Error()), "user has been suspended") { - err = p.onTwitterUsernameNotFound(ctx, username) - if err != nil { - return errors.Wrap(err, "error updating cached user state") - } - return nil - } - - return errors.Wrap(err, "error getting user info from twitter") - } - - err = p.updateCachedTwitterUser(ctx, user, nil) - if err != nil { - return errors.Wrap(err, "error updating cached user state") - } - return nil -} - -func (p *service) updateCachedTwitterUser(ctx context.Context, user *twitter_lib.User, newTipAccount *common.Account) error { - mu := p.userLocks.Get([]byte(user.Username)) - mu.Lock() - defer mu.Unlock() - - record, err := p.data.GetTwitterUserByUsername(ctx, user.Username) - switch err { - case twitter.ErrUserNotFound: - if newTipAccount == nil { - return errors.New("tip account must be present for newly registered twitter users") - } - - record = &twitter.Record{ - Username: user.Username, - } - - fallthrough - case nil: - record.Name = user.Name - record.ProfilePicUrl = user.ProfileImageUrl - record.VerifiedType = toProtoVerifiedType(user.VerifiedType) - record.FollowerCount = uint32(user.PublicMetrics.FollowersCount) - - if newTipAccount != nil { - record.TipAddress = newTipAccount.PublicKey().ToBase58() - } - default: - return errors.Wrap(err, "error getting cached twitter user") - } - - err = p.data.SaveTwitterUser(ctx, record) - switch err { - case nil, twitter.ErrDuplicateTipAddress: - return err - default: - return errors.Wrap(err, "error updating cached twitter user") - } -} - -func (p *service) findNewRegistrationTweets(ctx context.Context) ([]*twitter_lib.Tweet, error) { - var pageToken *string - var res []*twitter_lib.Tweet - for { - tweets, nextPageToken, err := p.twitterClient.SearchRecentTweets( - ctx, - tipCardRegistrationPrefix, - maxTweetSearchResults, - pageToken, - ) - if err != nil { - return nil, errors.Wrap(err, "error searching tweets") - } - - for _, tweet := range tweets { - isTweetProcessed, err := p.data.IsTweetProcessed(ctx, tweet.ID) - if err != nil { - return nil, errors.Wrap(err, "error checking if tweet is processed") - } else if isTweetProcessed { - // Found a checkpoint - return res, nil - } - - res = append([]*twitter_lib.Tweet{tweet}, res...) - } - - if nextPageToken == nil { - return res, nil - } - pageToken = nextPageToken - } -} - -func (p *service) findVerifiedTipAccountRegisteredInTweet(ctx context.Context, tweet *twitter_lib.Tweet) (*common.Account, *uuid.UUID, error) { - if tweet.IsRetweet() { - return nil, nil, errTwitterRegistrationNotFound - } - - tweetParts := strings.Fields(tweet.Text) - for _, tweetPart := range tweetParts { - // Look for the well-known prefix to indicate a potential registration value - - if !strings.HasPrefix(tweetPart, tipCardRegistrationPrefix) { - continue - } - - // Parse out the individual components of the registration value - - tweetPart = strings.TrimSuffix(tweetPart, ".") - registrationParts := strings.Split(tweetPart, ":") - if len(registrationParts) != 4 { - return nil, nil, errTwitterInvalidRegistrationValue - } - - addressString := registrationParts[1] - nonceString := registrationParts[2] - signatureString := registrationParts[3] - - decodedAddress, err := base58.Decode(addressString) - if err != nil { - return nil, nil, errTwitterInvalidRegistrationValue - } - if len(decodedAddress) != 32 { - return nil, nil, errTwitterInvalidRegistrationValue - } - tipAccount, _ := common.NewAccountFromPublicKeyBytes(decodedAddress) - - decodedNonce, err := base58.Decode(nonceString) - if err != nil { - return nil, nil, errTwitterInvalidRegistrationValue - } - if len(decodedNonce) != 16 { - return nil, nil, errTwitterInvalidRegistrationValue - } - nonce, _ := uuid.FromBytes(decodedNonce) - - decodedSignature, err := base58.Decode(signatureString) - if err != nil { - return nil, nil, errTwitterInvalidRegistrationValue - } - if len(decodedSignature) != 64 { - return nil, nil, errTwitterInvalidRegistrationValue - } - - // Validate the components of the registration value - - var tipAuthority *common.Account - accountInfoRecord, err := p.data.GetAccountInfoByTokenAddress(ctx, tipAccount.PublicKey().ToBase58()) - switch err { - case nil: - if accountInfoRecord.AccountType != commonpb.AccountType_PRIMARY { - return nil, nil, errTwitterInvalidRegistrationValue - } - - tipAuthority, err = common.NewAccountFromPublicKeyString(accountInfoRecord.AuthorityAccount) - if err != nil { - return nil, nil, errors.Wrap(err, "invalid tip authority account") - } - case account.ErrAccountInfoNotFound: - return nil, nil, errTwitterInvalidRegistrationValue - default: - return nil, nil, errors.Wrap(err, "error getting account info") - } - - if !ed25519.Verify(tipAuthority.PublicKey().ToBytes(), nonce[:], decodedSignature) { - return nil, nil, errTwitterInvalidRegistrationValue - } - - return tipAccount, &nonce, nil - } - - return nil, nil, errTwitterRegistrationNotFound -} - -func (p *service) sendRegistrationSuccessReply(ctx context.Context, regristrationTweetId, username string) error { - // todo: localize this - message := fmt.Sprintf( - "@%s your X account is now connected. Share this link to receive tips: https://tipcard.getcode.com/x/%s", - username, - username, - ) - _, err := p.twitterClient.SendReply(ctx, regristrationTweetId, message) - return err -} - -func (p *service) onTwitterUsernameNotFound(ctx context.Context, username string) error { - record, err := p.data.GetTwitterUserByUsername(ctx, username) - switch err { - case nil: - case twitter.ErrUserNotFound: - return nil - default: - return errors.Wrap(err, "error getting cached twitter user") - } - - record.LastUpdatedAt = time.Now() - - err = p.data.SaveTwitterUser(ctx, record) - if err != nil { - return errors.Wrap(err, "error updating cached twitter user") - } - return nil -} - -func toProtoVerifiedType(value string) userpb.TwitterUser_VerifiedType { - switch value { - case "blue": - return userpb.TwitterUser_BLUE - case "business": - return userpb.TwitterUser_BUSINESS - case "government": - return userpb.TwitterUser_GOVERNMENT - default: - return userpb.TwitterUser_NONE - } -} diff --git a/pkg/code/async/webhook/service.go b/pkg/code/async/webhook/service.go index 61518849..8b685f49 100644 --- a/pkg/code/async/webhook/service.go +++ b/pkg/code/async/webhook/service.go @@ -9,7 +9,7 @@ import ( "github.com/code-payments/code-server/pkg/code/async" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/server/grpc/messaging" + "github.com/code-payments/code-server/pkg/code/server/messaging" sync_util "github.com/code-payments/code-server/pkg/sync" ) diff --git a/pkg/code/async/webhook/worker_test.go b/pkg/code/async/webhook/worker_test.go index 2e49693c..e36d7401 100644 --- a/pkg/code/async/webhook/worker_test.go +++ b/pkg/code/async/webhook/worker_test.go @@ -9,11 +9,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/testutil" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/webhook" - "github.com/code-payments/code-server/pkg/code/server/grpc/messaging" + "github.com/code-payments/code-server/pkg/code/server/messaging" webhook_util "github.com/code-payments/code-server/pkg/code/webhook" + "github.com/code-payments/code-server/pkg/testutil" ) func TestWorker_HappyPath(t *testing.T) { diff --git a/pkg/code/auth/signature.go b/pkg/code/auth/signature.go index 9660e8bf..1ab98385 100644 --- a/pkg/code/auth/signature.go +++ b/pkg/code/auth/signature.go @@ -15,8 +15,6 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/storage" "github.com/code-payments/code-server/pkg/metrics" ) @@ -59,39 +57,6 @@ func (v *RPCSignatureVerifier) Authenticate(ctx context.Context, owner *common.A return nil } -// AuthorizeDataAccess authenticates and authorizes that an owner account can -// access data in a container. -func (v *RPCSignatureVerifier) AuthorizeDataAccess(ctx context.Context, dataContainerID *user.DataContainerID, owner *common.Account, message proto.Message, signature *commonpb.Signature) error { - defer metrics.TraceMethodCall(ctx, metricsStructName, "AuthorizeDataAccess").End() - - log := v.log.WithFields(logrus.Fields{ - "method": "AuthorizeDataAccess", - "data_container": dataContainerID.String(), - "owner_account": owner.PublicKey().ToBase58(), - }) - - isSignatureValid, err := v.isSignatureVerifiedProtoMessage(owner, message, signature) - if err != nil { - log.WithError(err).Warn("failure verifying signature") - return status.Error(codes.Internal, "") - } - - if !isSignatureValid { - return status.Error(codes.Unauthenticated, "") - } - - dataContainer, err := v.data.GetUserDataContainerByID(ctx, dataContainerID) - if err == storage.ErrNotFound { - return status.Error(codes.PermissionDenied, "") - } else if err != nil { - log.WithError(err).Warn("failure checking data container ownership") - return status.Error(codes.Internal, "") - } else if owner.PublicKey().ToBase58() != dataContainer.OwnerAccount { - return status.Error(codes.PermissionDenied, "") - } - return nil -} - // marshalStrategy is a strategy for marshalling protobuf messages for signature // verification type marshalStrategy func(proto.Message) ([]byte, error) diff --git a/pkg/code/auth/signature_test.go b/pkg/code/auth/signature_test.go index 191500e9..83394573 100644 --- a/pkg/code/auth/signature_test.go +++ b/pkg/code/auth/signature_test.go @@ -3,7 +3,6 @@ package auth import ( "context" "testing" - "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -14,8 +13,6 @@ import ( messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/storage" "github.com/code-payments/code-server/pkg/testutil" ) @@ -68,64 +65,3 @@ func TestAuthenticate(t *testing.T) { testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) } } - -func TestAuthorizeDataAccess(t *testing.T) { - for _, marshalStrategy := range defaultMarshalStrategies { - env := setup(t) - - dataContainerID := user.NewDataContainerID() - phoneNumber := "+11234567890" - - ownerAccount := testutil.NewRandomAccount(t) - - maliciousAccount := testutil.NewRandomAccount(t) - - msgValue, _ := uuid.New().MarshalBinary() - msg := &messagingpb.MessageId{ - Value: msgValue, - } - - msgBytes, err := marshalStrategy(msg) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(msgBytes) - require.NoError(t, err) - signatureProto := &commonpb.Signature{ - Value: signature, - } - - // Data container doesn't exist - err = env.verifier.AuthorizeDataAccess(env.ctx, dataContainerID, ownerAccount, msg, signatureProto) - assert.Error(t, err) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) - - require.NoError(t, env.data.PutUserDataContainer(env.ctx, &storage.Record{ - ID: dataContainerID, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - })) - - // Successful authorization - err = env.verifier.AuthorizeDataAccess(env.ctx, dataContainerID, ownerAccount, msg, signatureProto) - assert.NoError(t, err) - - signature, err = maliciousAccount.Sign(msgBytes) - require.NoError(t, err) - signatureProto = &commonpb.Signature{ - Value: signature, - } - - // Token account doesn't own data container - err = env.verifier.AuthorizeDataAccess(env.ctx, dataContainerID, maliciousAccount, msg, signatureProto) - assert.Error(t, err) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) - - // Signature doesn't match public key - err = env.verifier.AuthorizeDataAccess(env.ctx, dataContainerID, ownerAccount, msg, signatureProto) - assert.Error(t, err) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - } -} diff --git a/pkg/code/balance/calculator_test.go b/pkg/code/balance/calculator_test.go index 6251f93d..c4dd544f 100644 --- a/pkg/code/balance/calculator_test.go +++ b/pkg/code/balance/calculator_test.go @@ -18,9 +18,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/deposit" "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/payment" "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/pointer" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/testutil" @@ -31,7 +29,7 @@ func TestDefaultCalculationMethods_NewCodeAccount(t *testing.T) { vmAccount := testutil.NewRandomAccount(t) newOwnerAccount := testutil.NewRandomAccount(t) - newTokenAccount, err := newOwnerAccount.ToTimelockVault(vmAccount, common.KinMintAccount) + newTokenAccount, err := newOwnerAccount.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) data := &balanceTestData{ @@ -64,7 +62,7 @@ func TestDefaultCalculationMethods_DepositFromExternalWallet(t *testing.T) { vmAccount := testutil.NewRandomAccount(t) owner := testutil.NewRandomAccount(t) - depositAccount, err := owner.ToTimelockVault(vmAccount, common.KinMintAccount) + depositAccount, err := owner.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) @@ -108,19 +106,19 @@ func TestDefaultCalculationMethods_MultipleIntents(t *testing.T) { vmAccount := testutil.NewRandomAccount(t) owner1 := testutil.NewRandomAccount(t) - a1, err := owner1.ToTimelockVault(vmAccount, common.KinMintAccount) + a1, err := owner1.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) owner2 := testutil.NewRandomAccount(t) - a2, err := owner2.ToTimelockVault(vmAccount, common.KinMintAccount) + a2, err := owner2.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) owner3 := testutil.NewRandomAccount(t) - a3, err := owner3.ToTimelockVault(vmAccount, common.KinMintAccount) + a3, err := owner3.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) owner4 := testutil.NewRandomAccount(t) - a4, err := owner4.ToTimelockVault(vmAccount, common.KinMintAccount) + a4, err := owner4.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) @@ -208,11 +206,11 @@ func TestDefaultCalculationMethods_BackAndForth(t *testing.T) { vmAccount := testutil.NewRandomAccount(t) owner1 := testutil.NewRandomAccount(t) - a1, err := owner1.ToTimelockVault(vmAccount, common.KinMintAccount) + a1, err := owner1.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) owner2 := testutil.NewRandomAccount(t) - a2, err := owner2.ToTimelockVault(vmAccount, common.KinMintAccount) + a2, err := owner2.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) @@ -266,7 +264,7 @@ func TestDefaultCalculationMethods_SelfPayments(t *testing.T) { vmAccount := testutil.NewRandomAccount(t) ownerAccount := testutil.NewRandomAccount(t) - tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, common.KinMintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) @@ -311,7 +309,7 @@ func TestDefaultCalculationMethods_NotManagedByCode(t *testing.T) { vmAccount := testutil.NewRandomAccount(t) ownerAccount := testutil.NewRandomAccount(t) - tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, common.KinMintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, common.CoreMintAccount) require.NoError(t, err) data := &balanceTestData{ @@ -375,7 +373,7 @@ func TestGetAggregatedBalances(t *testing.T) { expectedPrivateBalance += balance } - timelockAccounts, err := authority.GetTimelockAccounts(vmAccount, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(vmAccount, common.CoreMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -385,7 +383,7 @@ func TestGetAggregatedBalances(t *testing.T) { OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: authority.PublicKey().ToBase58(), TokenAccount: timelockRecord.VaultAddress, - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: accountType, } if accountType == commonpb.AccountType_RELATIONSHIP { @@ -440,7 +438,7 @@ func setupBalanceTestEnv(t *testing.T) (env balanceTestEnv) { func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestData) { for _, owner := range data.codeUsers { - timelockAccounts, err := owner.GetTimelockAccounts(data.vmAccount, common.KinMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(data.vmAccount, common.CoreMintAccount) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() timelockRecord.VaultState = timelock_token_v1.StateLocked @@ -451,7 +449,7 @@ func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestDat OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: owner.PublicKey().ToBase58(), TokenAccount: timelockRecord.VaultAddress, - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_PRIMARY, } require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) @@ -464,12 +462,12 @@ func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestDat IntentId: txn.intentID, IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "owner", - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ + SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ DestinationOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), DestinationTokenAccount: txn.destination.PublicKey().ToBase58(), Quantity: txn.quantity, - ExchangeCurrency: currency.KIN, + ExchangeCurrency: common.CoreMintSymbol, ExchangeRate: 1.0, NativeAmount: 1.0, UsdMarketValue: 1.0, @@ -487,40 +485,14 @@ func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestDat ActionType: action.PrivateTransfer, Source: txn.source.PublicKey().ToBase58(), - Destination: &intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount, - Quantity: &intentRecord.SendPrivatePaymentMetadata.Quantity, + Destination: &intentRecord.SendPublicPaymentMetadata.DestinationTokenAccount, + Quantity: &intentRecord.SendPublicPaymentMetadata.Quantity, State: txn.actionState, } require.NoError(t, env.data.PutAllActions(env.ctx, actionRecord)) } - // We have an intent, and it's confirmed, so a payment record exists - if len(txn.intentID) > 0 && txn.intentState == intent.StateConfirmed { - paymentRecord := &payment.Record{ - Source: txn.source.PublicKey().ToBase58(), - Destination: txn.destination.PublicKey().ToBase58(), - Quantity: txn.quantity, - - Rendezvous: txn.intentID, - IsExternal: false, - - TransactionId: fmt.Sprintf("txn%d", i), - - ConfirmationState: txn.transactionState, - - // Below fields are irrelevant and can be set to whatever - ExchangeCurrency: string(currency.KIN), - ExchangeRate: 1.0, - UsdMarketValue: 1.0, - - BlockId: 12345, - - CreatedAt: time.Now(), - } - require.NoError(t, env.data.CreatePayment(env.ctx, paymentRecord)) - } - // There's no intent, so we have an external deposit if len(txn.intentID) == 0 && txn.transactionState != transaction.ConfirmationUnknown { depositRecord := &deposit.Record{ diff --git a/pkg/code/chat/chat.go b/pkg/code/chat/chat.go deleted file mode 100644 index e23f6555..00000000 --- a/pkg/code/chat/chat.go +++ /dev/null @@ -1,60 +0,0 @@ -package chat - -import "github.com/code-payments/code-server/pkg/code/localization" - -const ( - CashTransactionsName = "Cash Transactions" // Renamed to Cash Payments on client - CodeTeamName = "Code Team" - KinPurchasesName = "Kin Purchases" - PaymentsName = "Payments" // Renamed to Web Payments on client - TipsName = "Tips" - - // Test chats used for unit/integration testing only - TestCantMuteName = "TestCantMute" - TestCantUnsubscribeName = "TestCantUnsubscribe" -) - -var ( - InternalChatProperties = map[string]struct { - TitleLocalizationKey string - CanMute bool - CanUnsubscribe bool - }{ - CashTransactionsName: { - TitleLocalizationKey: localization.ChatTitleCashTransactions, - CanMute: true, - CanUnsubscribe: false, - }, - CodeTeamName: { - TitleLocalizationKey: localization.ChatTitleCodeTeam, - CanMute: true, - CanUnsubscribe: false, - }, - KinPurchasesName: { - TitleLocalizationKey: localization.ChatTitleKinPurchases, - CanMute: true, - CanUnsubscribe: false, - }, - PaymentsName: { - TitleLocalizationKey: localization.ChatTitlePayments, - CanMute: true, - CanUnsubscribe: false, - }, - TipsName: { - TitleLocalizationKey: localization.ChatTitleTips, - CanMute: true, - CanUnsubscribe: false, - }, - - TestCantMuteName: { - TitleLocalizationKey: "n/a", - CanMute: false, - CanUnsubscribe: true, - }, - TestCantUnsubscribeName: { - TitleLocalizationKey: "n/a", - CanMute: true, - CanUnsubscribe: false, - }, - } -) diff --git a/pkg/code/chat/message_blockchain.go b/pkg/code/chat/message_blockchain.go deleted file mode 100644 index 710bef6c..00000000 --- a/pkg/code/chat/message_blockchain.go +++ /dev/null @@ -1,32 +0,0 @@ -package chat - -import ( - "time" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/thirdparty" -) - -// ToBlockchainMessage takes a raw blockchain message and turns it into a protobuf -// chat message that can be injected into the merchant domain's chat. -func ToBlockchainMessage( - signature string, - sender *common.Account, - blockchainMessage *thirdparty.NaclBoxBlockchainMessage, - ts time.Time, -) (*chatpb.ChatMessage, error) { - content := []*chatpb.Content{ - { - Type: &chatpb.Content_NaclBox{ - NaclBox: &chatpb.NaclBoxEncryptedContent{ - PeerPublicKey: sender.ToProto(), - Nonce: blockchainMessage.Nonce, - EncryptedPayload: blockchainMessage.EncryptedMessage, - }, - }, - }, - } - return newProtoChatMessage(signature, content, ts) -} diff --git a/pkg/code/chat/message_cash_transactions.go b/pkg/code/chat/message_cash_transactions.go deleted file mode 100644 index 1976d9d5..00000000 --- a/pkg/code/chat/message_cash_transactions.go +++ /dev/null @@ -1,167 +0,0 @@ -package chat - -import ( - "context" - "strings" - - "github.com/pkg/errors" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/intent" -) - -// SendCashTransactionsExchangeMessage sends a message to the Cash Transactions -// chat with exchange data content related to the submitted intent. Intents that -// don't belong in the Cash Transactions chat will be ignored. -// -// Note: Tests covered in SubmitIntent history tests -func SendCashTransactionsExchangeMessage(ctx context.Context, data code_data.Provider, intentRecord *intent.Record) error { - messageId := intentRecord.IntentId - - exchangeData, ok := getExchangeDataFromIntent(intentRecord) - if !ok { - return nil - } - - verbByMessageReceiver := make(map[string]chatpb.ExchangeDataContent_Verb) - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - if intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - // Micro payment messages exist in merchant domain-specific chats - return nil - } else if intentRecord.SendPrivatePaymentMetadata.IsTip { - // Tip messages exist in a tip-specific chat - return nil - } else if intentRecord.SendPrivatePaymentMetadata.IsWithdrawal { - if intentRecord.InitiatorOwnerAccount == intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount { - // This is a top up for a public withdawal - return nil - } - - verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_WITHDREW - if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) > 0 { - destinationAccountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount) - if err != nil { - return err - } else if destinationAccountInfoRecord.AccountType != commonpb.AccountType_RELATIONSHIP { - // Relationship accounts payments will show up in the verified - // merchant chat - verbByMessageReceiver[intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount] = chatpb.ExchangeDataContent_DEPOSITED - } - } - } else if intentRecord.SendPrivatePaymentMetadata.IsRemoteSend { - verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_SENT - } else { - verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_GAVE - if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) > 0 { - verbByMessageReceiver[intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount] = chatpb.ExchangeDataContent_RECEIVED - } - } - - case intent.SendPublicPayment: - if intentRecord.SendPublicPaymentMetadata.IsWithdrawal { - if intentRecord.InitiatorOwnerAccount == intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount { - // This is an internal movement of funds across the same Code user's public accounts - return nil - } - - verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_WITHDREW - if len(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) > 0 { - destinationAccountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, intentRecord.SendPublicPaymentMetadata.DestinationTokenAccount) - if err != nil { - return err - } else if destinationAccountInfoRecord.AccountType != commonpb.AccountType_RELATIONSHIP { - // Relationship accounts payments will show up in the verified - // merchant chat - verbByMessageReceiver[intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount] = chatpb.ExchangeDataContent_DEPOSITED - } - } - } - - case intent.ReceivePaymentsPublicly: - if intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend { - if intentRecord.ReceivePaymentsPubliclyMetadata.IsReturned { - verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_RETURNED - } else if intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard { - giftCardIssuedIntentRecord, err := data.GetOriginalGiftCardIssuedIntent(ctx, intentRecord.ReceivePaymentsPubliclyMetadata.Source) - if err != nil { - return errors.Wrap(err, "error getting original gift card issued intent") - } - - chatId := chat.GetChatId(CashTransactionsName, giftCardIssuedIntentRecord.InitiatorOwnerAccount, true) - - err = data.DeleteChatMessage(ctx, chatId, giftCardIssuedIntentRecord.IntentId) - if err != nil { - return errors.Wrap(err, "error deleting chat message") - } - return nil - } else { - verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_RECEIVED - } - } - - case intent.MigrateToPrivacy2022: - if intentRecord.MigrateToPrivacy2022Metadata.Quantity > 0 { - verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_DEPOSITED - } - - case intent.ExternalDeposit: - messageId = strings.Split(messageId, "-")[0] - destinationAccountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, intentRecord.ExternalDepositMetadata.DestinationTokenAccount) - if err != nil { - return err - } else if destinationAccountInfoRecord.AccountType != commonpb.AccountType_RELATIONSHIP { - // Relationship accounts payments will show up in the verified - // merchant chat - verbByMessageReceiver[intentRecord.ExternalDepositMetadata.DestinationOwnerAccount] = chatpb.ExchangeDataContent_DEPOSITED - } - - default: - return nil - } - - for account, verb := range verbByMessageReceiver { - receiver, err := common.NewAccountFromPublicKeyString(account) - if err != nil { - return err - } - - content := []*chatpb.Content{ - { - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: verb, - ExchangeData: &chatpb.ExchangeDataContent_Exact{ - Exact: exchangeData, - }, - }, - }, - }, - } - protoMessage, err := newProtoChatMessage(messageId, content, intentRecord.CreatedAt) - if err != nil { - return errors.Wrap(err, "error creating proto chat message") - } - - _, err = SendChatMessage( - ctx, - data, - CashTransactionsName, - chat.ChatTypeInternal, - true, - receiver, - protoMessage, - true, - ) - if err != nil && err != chat.ErrMessageAlreadyExists { - return errors.Wrap(err, "error persisting chat message") - } - } - - return nil -} diff --git a/pkg/code/chat/message_code_team.go b/pkg/code/chat/message_code_team.go deleted file mode 100644 index 536370a3..00000000 --- a/pkg/code/chat/message_code_team.go +++ /dev/null @@ -1,70 +0,0 @@ -package chat - -import ( - "context" - - "github.com/pkg/errors" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/localization" -) - -// SendCodeTeamMessage sends a message to the Code Team chat. -func SendCodeTeamMessage(ctx context.Context, data code_data.Provider, receiver *common.Account, chatMessage *chatpb.ChatMessage) (bool, error) { - return SendChatMessage( - ctx, - data, - CodeTeamName, - chat.ChatTypeInternal, - true, - receiver, - chatMessage, - false, - ) -} - -// ToWelcomeBonusMessage turns the intent record into a welcome bonus chat message -// to be inserted into the Code Team chat. -func ToWelcomeBonusMessage(intentRecord *intent.Record) (*chatpb.ChatMessage, error) { - return newIncentiveMessage(localization.ChatMessageWelcomeBonus, intentRecord) -} - -// ToReferralBonusMessage turns the intent record into a referral bonus chat message -// to be inserted into the Code Team chat. -func ToReferralBonusMessage(intentRecord *intent.Record) (*chatpb.ChatMessage, error) { - return newIncentiveMessage(localization.ChatMessageReferralBonus, intentRecord) -} - -func newIncentiveMessage(localizedTextKey string, intentRecord *intent.Record) (*chatpb.ChatMessage, error) { - exchangeData, ok := getExchangeDataFromIntent(intentRecord) - if !ok { - return nil, errors.New("exchange data not available") - } - - content := []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: localizedTextKey, - }, - }, - }, - { - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: chatpb.ExchangeDataContent_RECEIVED, - ExchangeData: &chatpb.ExchangeDataContent_Exact{ - Exact: exchangeData, - }, - }, - }, - }, - } - - return newProtoChatMessage(intentRecord.IntentId, content, intentRecord.CreatedAt) -} diff --git a/pkg/code/chat/message_kin_purchases.go b/pkg/code/chat/message_kin_purchases.go deleted file mode 100644 index f9a2c6fd..00000000 --- a/pkg/code/chat/message_kin_purchases.go +++ /dev/null @@ -1,102 +0,0 @@ -package chat - -import ( - "context" - "time" - - "github.com/pkg/errors" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/localization" -) - -// GetKinPurchasesChatId returns the chat ID for the Kin Purchases chat for a -// given owner account -func GetKinPurchasesChatId(owner *common.Account) chat.ChatId { - return chat.GetChatId(KinPurchasesName, owner.PublicKey().ToBase58(), true) -} - -// SendKinPurchasesMessage sends a message to the Kin Purchases chat. -func SendKinPurchasesMessage(ctx context.Context, data code_data.Provider, receiver *common.Account, chatMessage *chatpb.ChatMessage) (bool, error) { - return SendChatMessage( - ctx, - data, - KinPurchasesName, - chat.ChatTypeInternal, - true, - receiver, - chatMessage, - false, - ) -} - -// ToUsdcDepositedMessage turns details of a USDC deposit transaction into a chat -// message to be inserted into the Kin Purchases chat. -func ToUsdcDepositedMessage(signature string, ts time.Time) (*chatpb.ChatMessage, error) { - content := []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: localization.ChatMessageUsdcDeposited, - }, - }, - }, - } - return newProtoChatMessage(signature, content, ts) -} - -// NewUsdcBeingConvertedMessage generates a new message generated upon initiating -// a USDC swap to be inserted into the Kin Purchases chat. -func NewUsdcBeingConvertedMessage(ts time.Time) (*chatpb.ChatMessage, error) { - messageId, err := common.NewRandomAccount() - if err != nil { - return nil, err - } - - content := []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: localization.ChatMessageUsdcBeingConverted, - }, - }, - }, - } - return newProtoChatMessage(messageId.PublicKey().ToBase58(), content, ts) -} - -// ToKinAvailableForUseMessage turns details of a USDC swap transaction into a -// chat message to be inserted into the Kin Purchases chat. -func ToKinAvailableForUseMessage(signature string, ts time.Time, purchases ...*transactionpb.ExchangeDataWithoutRate) (*chatpb.ChatMessage, error) { - if len(purchases) == 0 { - return nil, errors.New("no purchases for kin available chat message") - } - - content := []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: localization.ChatMessageKinAvailableForUse, - }, - }, - }, - } - for _, purchase := range purchases { - content = append(content, &chatpb.Content{ - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: chatpb.ExchangeDataContent_PURCHASED, - ExchangeData: &chatpb.ExchangeDataContent_Partial{ - Partial: purchase, - }, - }, - }, - }) - } - return newProtoChatMessage(signature, content, ts) -} diff --git a/pkg/code/chat/message_merchant.go b/pkg/code/chat/message_merchant.go deleted file mode 100644 index b4504c39..00000000 --- a/pkg/code/chat/message_merchant.go +++ /dev/null @@ -1,187 +0,0 @@ -package chat - -import ( - "context" - "strings" - - "github.com/pkg/errors" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/intent" -) - -// SendMerchantExchangeMessage sends a message to the merchant's chat with -// exchange data content related to the submitted intent. Intents that -// don't belong in the merchant chat will be ignored. The set of chat messages -// that should be pushed are returned. -// -// Note: Tests covered in SubmitIntent history tests -func SendMerchantExchangeMessage(ctx context.Context, data code_data.Provider, intentRecord *intent.Record, actionRecords []*action.Record) ([]*MessageWithOwner, error) { - messageId := intentRecord.IntentId - - // There are three possible chats for a merchant: - // 1. Verified chat with a verified identifier that server has validated - // 2. Unverified chat with an unverified identifier - // 3. Fallback internal "Payments" chat when no identifier is provided - // These chats represent upgrades in functionality. From 2 to 3, we can enable - // an identifier. From 2 to 1, we can enable messaging from the merchant to the - // user, and guarantee the chat is clean with only messages originiating from - // the merchant. Representation in the UI may differ (ie. 2 and 3 are grouped), - // but this is the most flexible solution with the chat model. - chatTitle := PaymentsName - chatType := chat.ChatTypeInternal - isVerifiedChat := false - - exchangeData, ok := getExchangeDataFromIntent(intentRecord) - if !ok { - return nil, nil - } - - type verbAndExchangeData struct { - verb chatpb.ExchangeDataContent_Verb - exchangeData *transactionpb.ExchangeData - } - verbAndExchangeDataByMessageReceiver := make(map[string]*verbAndExchangeData) - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - if intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - paymentRequestRecord, err := data.GetRequest(ctx, intentRecord.IntentId) - if err != nil { - return nil, errors.Wrap(err, "error getting request record") - } - - if paymentRequestRecord.Domain != nil { - chatTitle = *paymentRequestRecord.Domain - chatType = chat.ChatTypeExternalApp - isVerifiedChat = paymentRequestRecord.IsVerified - } - - verbAndExchangeDataByMessageReceiver[intentRecord.InitiatorOwnerAccount] = &verbAndExchangeData{ - verb: chatpb.ExchangeDataContent_SPENT, - exchangeData: exchangeData, - } - receiveByOwner, err := getMicroPaymentReceiveExchangeDataByOwner(ctx, data, exchangeData, intentRecord, actionRecords) - if err != nil { - return nil, err - } - for owner, exchangeData := range receiveByOwner { - verbAndExchangeDataByMessageReceiver[owner] = &verbAndExchangeData{ - verb: chatpb.ExchangeDataContent_RECEIVED, - exchangeData: exchangeData, - } - } - } else if intentRecord.SendPrivatePaymentMetadata.IsWithdrawal { - if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) > 0 { - destinationAccountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount) - if err != nil { - return nil, err - } else if destinationAccountInfoRecord.AccountType == commonpb.AccountType_RELATIONSHIP { - // Relationship accounts only exist against verified merchants, - // and will have merchant payments appear in the verified merchant - // chat. - chatTitle = *destinationAccountInfoRecord.RelationshipTo - chatType = chat.ChatTypeExternalApp - isVerifiedChat = true - verbAndExchangeDataByMessageReceiver[intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount] = &verbAndExchangeData{ - verb: chatpb.ExchangeDataContent_RECEIVED, - exchangeData: exchangeData, - } - } - } - } - case intent.SendPublicPayment: - if intentRecord.SendPublicPaymentMetadata.IsWithdrawal { - if len(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) > 0 { - destinationAccountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, intentRecord.SendPublicPaymentMetadata.DestinationTokenAccount) - if err != nil { - return nil, err - } else if destinationAccountInfoRecord.AccountType == commonpb.AccountType_RELATIONSHIP { - // Relationship accounts only exist against verified merchants, - // and will have merchant payments appear in the verified merchant - // chat. - chatTitle = *destinationAccountInfoRecord.RelationshipTo - chatType = chat.ChatTypeExternalApp - isVerifiedChat = true - verbAndExchangeDataByMessageReceiver[intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount] = &verbAndExchangeData{ - verb: chatpb.ExchangeDataContent_RECEIVED, - exchangeData: exchangeData, - } - } - } - } - case intent.ExternalDeposit: - messageId = strings.Split(messageId, "-")[0] - destinationAccountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, intentRecord.ExternalDepositMetadata.DestinationTokenAccount) - if err != nil { - return nil, err - } else if destinationAccountInfoRecord.AccountType == commonpb.AccountType_RELATIONSHIP { - // Relationship accounts only exist against verified merchants, - // and will have merchant payments appear in the verified merchant - // chat. - chatTitle = *destinationAccountInfoRecord.RelationshipTo - chatType = chat.ChatTypeExternalApp - isVerifiedChat = true - verbAndExchangeDataByMessageReceiver[intentRecord.ExternalDepositMetadata.DestinationOwnerAccount] = &verbAndExchangeData{ - verb: chatpb.ExchangeDataContent_RECEIVED, - exchangeData: exchangeData, - } - } - default: - return nil, nil - } - - var messagesToPush []*MessageWithOwner - for account, verbAndExchangeData := range verbAndExchangeDataByMessageReceiver { - receiver, err := common.NewAccountFromPublicKeyString(account) - if err != nil { - return nil, err - } - - content := []*chatpb.Content{ - { - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: verbAndExchangeData.verb, - ExchangeData: &chatpb.ExchangeDataContent_Exact{ - Exact: verbAndExchangeData.exchangeData, - }, - }, - }, - }, - } - protoMessage, err := newProtoChatMessage(messageId, content, intentRecord.CreatedAt) - if err != nil { - return nil, errors.Wrap(err, "error creating proto chat message") - } - - canPush, err := SendChatMessage( - ctx, - data, - chatTitle, - chatType, - isVerifiedChat, - receiver, - protoMessage, - verbAndExchangeData.verb != chatpb.ExchangeDataContent_RECEIVED || !isVerifiedChat, - ) - if err != nil && err != chat.ErrMessageAlreadyExists { - return nil, errors.Wrap(err, "error persisting chat message") - } - - if canPush { - messagesToPush = append(messagesToPush, &MessageWithOwner{ - Owner: receiver, - Title: chatTitle, - Message: protoMessage, - }) - } - } - return messagesToPush, nil -} diff --git a/pkg/code/chat/message_tips.go b/pkg/code/chat/message_tips.go deleted file mode 100644 index b9984a9b..00000000 --- a/pkg/code/chat/message_tips.go +++ /dev/null @@ -1,93 +0,0 @@ -package chat - -import ( - "context" - - "github.com/pkg/errors" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/intent" -) - -// SendTipsExchangeMessage sends a message to the Tips chat with exchange data -// content related to the submitted intent. Intents that don't belong in the -// Tips chat will be ignored. -// -// Note: Tests covered in SubmitIntent history tests -func SendTipsExchangeMessage(ctx context.Context, data code_data.Provider, intentRecord *intent.Record) ([]*MessageWithOwner, error) { - messageId := intentRecord.IntentId - - exchangeData, ok := getExchangeDataFromIntent(intentRecord) - if !ok { - return nil, nil - } - - verbByMessageReceiver := make(map[string]chatpb.ExchangeDataContent_Verb) - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - if !intentRecord.SendPrivatePaymentMetadata.IsTip { - // Not a tip - return nil, nil - } - - verbByMessageReceiver[intentRecord.InitiatorOwnerAccount] = chatpb.ExchangeDataContent_SENT_TIP - if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) > 0 { - verbByMessageReceiver[intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount] = chatpb.ExchangeDataContent_RECEIVED_TIP - } - default: - return nil, nil - } - - var messagesToPush []*MessageWithOwner - for account, verb := range verbByMessageReceiver { - receiver, err := common.NewAccountFromPublicKeyString(account) - if err != nil { - return nil, err - } - - content := []*chatpb.Content{ - { - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: verb, - ExchangeData: &chatpb.ExchangeDataContent_Exact{ - Exact: exchangeData, - }, - }, - }, - }, - } - protoMessage, err := newProtoChatMessage(messageId, content, intentRecord.CreatedAt) - if err != nil { - return nil, errors.Wrap(err, "error creating proto chat message") - } - - canPush, err := SendChatMessage( - ctx, - data, - TipsName, - chat.ChatTypeInternal, - true, - receiver, - protoMessage, - verb != chatpb.ExchangeDataContent_RECEIVED_TIP, - ) - if err != nil && err != chat.ErrMessageAlreadyExists { - return nil, errors.Wrap(err, "error persisting chat message") - } - - if canPush { - messagesToPush = append(messagesToPush, &MessageWithOwner{ - Owner: receiver, - Title: TipsName, - Message: protoMessage, - }) - } - } - - return messagesToPush, nil -} diff --git a/pkg/code/chat/sender.go b/pkg/code/chat/sender.go deleted file mode 100644 index 41da0902..00000000 --- a/pkg/code/chat/sender.go +++ /dev/null @@ -1,117 +0,0 @@ -package chat - -import ( - "context" - "errors" - "time" - - "github.com/mr-tron/base58" - "google.golang.org/protobuf/proto" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" -) - -// SendChatMessage sends a chat message to a receiving owner account. -// -// Note: This function is not responsible for push notifications. This method -// might be called within the context of a DB transaction, which might have -// unrelated failures. A hint as to whether a push should be sent is provided. -func SendChatMessage( - ctx context.Context, - data code_data.Provider, - chatTitle string, - chatType chat.ChatType, - isVerifiedChat bool, - receiver *common.Account, - protoMessage *chatpb.ChatMessage, - isSilentMessage bool, -) (canPushMessage bool, err error) { - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), isVerifiedChat) - - if protoMessage.Cursor != nil { - // Let the utilities and GetMessages RPC handle cursors - return false, errors.New("cursor must not be set") - } - - if err := protoMessage.Validate(); err != nil { - return false, err - } - - messageId := protoMessage.MessageId.Value - ts := protoMessage.Ts - - // Clear out extracted metadata as a space optimization - cloned := proto.Clone(protoMessage).(*chatpb.ChatMessage) - cloned.MessageId = nil - cloned.Ts = nil - cloned.Cursor = nil - - marshalled, err := proto.Marshal(cloned) - if err != nil { - return false, err - } - - canPersistMessage := true - canPushMessage = !isSilentMessage - - existingChatRecord, err := data.GetChatById(ctx, chatId) - switch err { - case nil: - canPersistMessage = !existingChatRecord.IsUnsubscribed - canPushMessage = canPushMessage && canPersistMessage && !existingChatRecord.IsMuted - case chat.ErrChatNotFound: - chatRecord := &chat.Chat{ - ChatId: chatId, - ChatType: chatType, - ChatTitle: chatTitle, - IsVerified: isVerifiedChat, - - CodeUser: receiver.PublicKey().ToBase58(), - - ReadPointer: nil, - IsMuted: false, - IsUnsubscribed: false, - - CreatedAt: time.Now(), - } - - err = data.PutChat(ctx, chatRecord) - if err != nil && err != chat.ErrChatAlreadyExists { - return false, err - } - default: - return false, err - } - - if canPersistMessage { - messageRecord := &chat.Message{ - ChatId: chatId, - - MessageId: base58.Encode(messageId), - Data: marshalled, - - IsSilent: isSilentMessage, - ContentLength: uint8(len(protoMessage.Content)), - - Timestamp: ts.AsTime(), - } - - err = data.PutChatMessage(ctx, messageRecord) - if err != nil { - return false, err - } - } - - if canPushMessage { - err = data.AddToBadgeCount(ctx, receiver.PublicKey().ToBase58(), 1) - if err != nil { - return false, err - } - } - - return canPushMessage, nil -} diff --git a/pkg/code/chat/sender_test.go b/pkg/code/chat/sender_test.go deleted file mode 100644 index 3438b3c8..00000000 --- a/pkg/code/chat/sender_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package chat - -import ( - "context" - "fmt" - "math/rand" - "testing" - "time" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/badgecount" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/testutil" -) - -func TestSendChatMessage_HappyPath(t *testing.T) { - env := setup(t) - - chatTitle := CodeTeamName - receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) - - var expectedBadgeCount int - for i := 0; i < 10; i++ { - chatMessage := newRandomChatMessage(t, i+1) - expectedBadgeCount += 1 - - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, false) - require.NoError(t, err) - - assert.True(t, canPush) - - assert.NotNil(t, chatMessage.MessageId) - assert.NotNil(t, chatMessage.Ts) - assert.Nil(t, chatMessage.Cursor) - - env.assertChatRecordSaved(t, chatTitle, receiver, true) - env.assertChatMessageRecordSaved(t, chatId, chatMessage, false) - env.assertBadgeCount(t, receiver, expectedBadgeCount) - } -} - -func TestSendChatMessage_VerifiedChat(t *testing.T) { - env := setup(t) - - chatTitle := CodeTeamName - receiver := testutil.NewRandomAccount(t) - - for _, isVerified := range []bool{true, false} { - chatMessage := newRandomChatMessage(t, 1) - _, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, isVerified, receiver, chatMessage, true) - require.NoError(t, err) - env.assertChatRecordSaved(t, chatTitle, receiver, isVerified) - } -} - -func TestSendChatMessage_SilentMessage(t *testing.T) { - env := setup(t) - - chatTitle := CodeTeamName - receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) - - for i, isSilent := range []bool{true, false} { - chatMessage := newRandomChatMessage(t, 1) - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, isSilent) - require.NoError(t, err) - assert.Equal(t, !isSilent, canPush) - env.assertChatMessageRecordSaved(t, chatId, chatMessage, isSilent) - env.assertBadgeCount(t, receiver, i) - } -} - -func TestSendChatMessage_MuteState(t *testing.T) { - env := setup(t) - - chatTitle := CodeTeamName - receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) - - for _, isMuted := range []bool{false, true} { - if isMuted { - env.muteChat(t, chatId) - } - - chatMessage := newRandomChatMessage(t, 1) - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, false) - require.NoError(t, err) - assert.Equal(t, !isMuted, canPush) - env.assertChatMessageRecordSaved(t, chatId, chatMessage, false) - env.assertBadgeCount(t, receiver, 1) - } -} - -func TestSendChatMessage_SubscriptionState(t *testing.T) { - env := setup(t) - - chatTitle := CodeTeamName - receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) - - for _, isUnsubscribed := range []bool{false, true} { - if isUnsubscribed { - env.unsubscribeFromChat(t, chatId) - } - - chatMessage := newRandomChatMessage(t, 1) - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, false) - require.NoError(t, err) - assert.Equal(t, !isUnsubscribed, canPush) - if isUnsubscribed { - env.assertChatMessageRecordNotSaved(t, chatId, chatMessage.MessageId) - } else { - env.assertChatMessageRecordSaved(t, chatId, chatMessage, false) - } - env.assertBadgeCount(t, receiver, 1) - } -} - -func TestSendChatMessage_InvalidProtoMessage(t *testing.T) { - env := setup(t) - - chatTitle := CodeTeamName - receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) - - chatMessage := newRandomChatMessage(t, 1) - chatMessage.Content = nil - - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, false) - assert.Error(t, err) - assert.False(t, canPush) - env.assertChatRecordNotSaved(t, chatId) - env.assertChatMessageRecordNotSaved(t, chatId, chatMessage.MessageId) - env.assertBadgeCount(t, receiver, 0) -} - -type testEnv struct { - ctx context.Context - data code_data.Provider -} - -func setup(t *testing.T) *testEnv { - return &testEnv{ - ctx: context.Background(), - data: code_data.NewTestDataProvider(), - } -} - -func newRandomChatMessage(t *testing.T, contentLength int) *chatpb.ChatMessage { - var content []*chatpb.Content - for i := 0; i < contentLength; i++ { - content = append(content, &chatpb.Content{ - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: fmt.Sprintf("key%d", rand.Uint32()), - }, - }, - }) - } - - msg, err := newProtoChatMessage(testutil.NewRandomAccount(t).PrivateKey().ToBase58(), content, time.Now()) - require.NoError(t, err) - return msg -} - -func (e *testEnv) assertChatRecordSaved(t *testing.T, chatTitle string, receiver *common.Account, isVerified bool) { - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), isVerified) - - chatRecord, err := e.data.GetChatById(e.ctx, chatId) - require.NoError(t, err) - - assert.Equal(t, chatId[:], chatRecord.ChatId[:]) - assert.Equal(t, chat.ChatTypeInternal, chatRecord.ChatType) - assert.Equal(t, chatTitle, chatRecord.ChatTitle) - assert.Equal(t, isVerified, chatRecord.IsVerified) - assert.Equal(t, receiver.PublicKey().ToBase58(), chatRecord.CodeUser) - assert.Nil(t, chatRecord.ReadPointer) - assert.False(t, chatRecord.IsMuted) - assert.False(t, chatRecord.IsUnsubscribed) -} - -func (e *testEnv) assertChatMessageRecordSaved(t *testing.T, chatId chat.ChatId, protoMessage *chatpb.ChatMessage, isSilent bool) { - messageRecord, err := e.data.GetChatMessage(e.ctx, chatId, base58.Encode(protoMessage.GetMessageId().Value)) - require.NoError(t, err) - - cloned := proto.Clone(protoMessage).(*chatpb.ChatMessage) - cloned.MessageId = nil - cloned.Ts = nil - cloned.Cursor = nil - - expectedData, err := proto.Marshal(cloned) - require.NoError(t, err) - - assert.Equal(t, messageRecord.MessageId, base58.Encode(protoMessage.GetMessageId().Value)) - assert.Equal(t, chatId[:], messageRecord.ChatId[:]) - assert.Equal(t, expectedData, messageRecord.Data) - assert.Equal(t, isSilent, messageRecord.IsSilent) - assert.EqualValues(t, messageRecord.ContentLength, len(protoMessage.Content)) - assert.Equal(t, messageRecord.Timestamp.Unix(), protoMessage.Ts.Seconds) -} - -func (e *testEnv) assertBadgeCount(t *testing.T, owner *common.Account, expected int) { - badgeCountRecord, err := e.data.GetBadgeCount(e.ctx, owner.PublicKey().ToBase58()) - if err == badgecount.ErrBadgeCountNotFound { - assert.Equal(t, 0, expected) - return - } - require.NoError(t, err) - assert.EqualValues(t, expected, badgeCountRecord.BadgeCount) -} - -func (e *testEnv) assertChatRecordNotSaved(t *testing.T, chatId chat.ChatId) { - _, err := e.data.GetChatById(e.ctx, chatId) - assert.Equal(t, chat.ErrChatNotFound, err) - -} - -func (e *testEnv) assertChatMessageRecordNotSaved(t *testing.T, chatId chat.ChatId, messageId *chatpb.ChatMessageId) { - _, err := e.data.GetChatMessage(e.ctx, chatId, base58.Encode(messageId.Value)) - assert.Equal(t, chat.ErrMessageNotFound, err) - -} - -func (e *testEnv) muteChat(t *testing.T, chatId chat.ChatId) { - require.NoError(t, e.data.SetChatMuteState(e.ctx, chatId, true)) -} - -func (e *testEnv) unsubscribeFromChat(t *testing.T, chatId chat.ChatId) { - require.NoError(t, e.data.SetChatSubscriptionState(e.ctx, chatId, false)) -} diff --git a/pkg/code/chat/util.go b/pkg/code/chat/util.go deleted file mode 100644 index d019984d..00000000 --- a/pkg/code/chat/util.go +++ /dev/null @@ -1,198 +0,0 @@ -package chat - -import ( - "context" - "time" - - "github.com/mr-tron/base58/base58" - "github.com/pkg/errors" - "google.golang.org/protobuf/types/known/timestamppb" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/intent" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" -) - -type MessageWithOwner struct { - Owner *common.Account - Title string - Message *chatpb.ChatMessage -} - -func newProtoChatMessage( - messageId string, - content []*chatpb.Content, - ts time.Time, -) (*chatpb.ChatMessage, error) { - decodedMessageId, err := base58.Decode(messageId) - if err != nil { - return nil, errors.Wrap(err, "error decoding message id") - } - - if len(decodedMessageId) != 32 && len(decodedMessageId) != 64 { - return nil, errors.Errorf("invalid message id length of %d", len(decodedMessageId)) - } - - msg := &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: decodedMessageId, - }, - Ts: timestamppb.New(ts), - Content: content, - } - - if err := msg.Validate(); err != nil { - return nil, errors.Wrap(err, "chat message failed validation") - } - - return msg, nil -} - -// todo: promote more broadly? -func getExchangeDataFromIntent(intentRecord *intent.Record) (*transactionpb.ExchangeData, bool) { - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - return &transactionpb.ExchangeData{ - Currency: string(intentRecord.SendPrivatePaymentMetadata.ExchangeCurrency), - ExchangeRate: intentRecord.SendPrivatePaymentMetadata.ExchangeRate, - NativeAmount: intentRecord.SendPrivatePaymentMetadata.NativeAmount, - Quarks: intentRecord.SendPrivatePaymentMetadata.Quantity, - }, true - case intent.SendPublicPayment: - return &transactionpb.ExchangeData{ - Currency: string(intentRecord.SendPublicPaymentMetadata.ExchangeCurrency), - ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate, - NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount, - Quarks: intentRecord.SendPublicPaymentMetadata.Quantity, - }, true - case intent.ReceivePaymentsPrivately: - return &transactionpb.ExchangeData{ - Currency: string(currency_lib.KIN), - ExchangeRate: 1.0, - NativeAmount: float64(intentRecord.ReceivePaymentsPrivatelyMetadata.Quantity) / kin.QuarksPerKin, - Quarks: intentRecord.ReceivePaymentsPrivatelyMetadata.Quantity, - }, true - case intent.ReceivePaymentsPublicly: - return &transactionpb.ExchangeData{ - Currency: string(intentRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeCurrency), - ExchangeRate: intentRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate, - NativeAmount: intentRecord.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount, - Quarks: intentRecord.ReceivePaymentsPubliclyMetadata.Quantity, - }, true - case intent.MigrateToPrivacy2022: - return &transactionpb.ExchangeData{ - Currency: string(currency_lib.KIN), - ExchangeRate: 1.0, - NativeAmount: float64(intentRecord.MigrateToPrivacy2022Metadata.Quantity) / kin.QuarksPerKin, - Quarks: intentRecord.MigrateToPrivacy2022Metadata.Quantity, - }, true - case intent.ExternalDeposit: - return &transactionpb.ExchangeData{ - Currency: string(currency_lib.KIN), - ExchangeRate: 1.0, - NativeAmount: float64(intentRecord.ExternalDepositMetadata.Quantity) / kin.QuarksPerKin, - Quarks: intentRecord.ExternalDepositMetadata.Quantity, - }, true - } - - return nil, false -} - -func getMicroPaymentReceiveExchangeDataByOwner( - ctx context.Context, - data code_data.Provider, - exchangeData *transactionpb.ExchangeData, - intentRecord *intent.Record, - actionRecords []*action.Record, -) (map[string]*transactionpb.ExchangeData, error) { - if intentRecord.IntentType != intent.SendPrivatePayment || !intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - return nil, errors.New("intent is not a micro payment") - } - - // Find the action record where the final payment is made - var thirdPartyPaymentAction *action.Record - for _, actionRecord := range actionRecords { - if actionRecord.ActionType != action.NoPrivacyWithdraw { - continue - } - - if *actionRecord.Destination == intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount { - thirdPartyPaymentAction = actionRecord - break - } - } - - // Should never happen if the intent is a micropayment - if thirdPartyPaymentAction == nil { - return nil, errors.New("payment action is missing") - } - - quarksByTokenAccount := make(map[string]uint64) - quarksByTokenAccount[*thirdPartyPaymentAction.Destination] = *thirdPartyPaymentAction.Quantity - - // Find and consolidate all fee payments into a quark amount by token account - var foundCodeFee bool - for _, actionRecord := range actionRecords { - if actionRecord.ActionType != action.NoPrivacyTransfer { - continue - } - - if actionRecord.Source != thirdPartyPaymentAction.Source { - continue - } - - // The first fee is always Code, and can be skipped - if !foundCodeFee { - foundCodeFee = true - continue - } - - quarksByTokenAccount[*actionRecord.Destination] += *actionRecord.Quantity - } - - // Consolidate quark amount by owner account - quarksByOwnerAccount := make(map[string]uint64) - for tokenAccount, quarks := range quarksByTokenAccount { - if tokenAccount == intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount { - if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) > 0 { - quarksByOwnerAccount[intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount] += quarks - } - continue - } - - accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, tokenAccount) - if err == nil { - quarksByOwnerAccount[accountInfoRecord.OwnerAccount] += quarks - } else if err != account.ErrAccountInfoNotFound { - return nil, err - } - } - - // Map result to an exchange data - res := make(map[string]*transactionpb.ExchangeData) - for ownerAccount, quarks := range quarksByOwnerAccount { - res[ownerAccount] = getExchangeDataInOtherQuarkAmount(exchangeData, quarks) - } - return res, nil -} - -func getExchangeDataInOtherQuarkAmount(original *transactionpb.ExchangeData, quarks uint64) *transactionpb.ExchangeData { - nativeAmount := original.NativeAmount - if original.Quarks != quarks { - nativeAmount = original.ExchangeRate * float64(quarks) / float64(kin.QuarksPerKin) - } - - return &transactionpb.ExchangeData{ - Currency: original.Currency, - ExchangeRate: original.ExchangeRate, - NativeAmount: nativeAmount, - Quarks: quarks, - } -} diff --git a/pkg/code/chat/util_test.go b/pkg/code/chat/util_test.go deleted file mode 100644 index 1d177ca4..00000000 --- a/pkg/code/chat/util_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package chat - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/intent" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/testutil" -) - -func TestGetMicroPaymentReceiveExchangeDataByOwner(t *testing.T) { - env := setup(t) - - micropaymentDestinationOwner := testutil.NewRandomAccount(t) - additionalCodeUserDestinationOwner := testutil.NewRandomAccount(t) - tempOutgoingAccount := testutil.NewRandomAccount(t) - - intentRecord := &intent.Record{ - IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - IntentType: intent.SendPrivatePayment, - - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - DestinationOwnerAccount: micropaymentDestinationOwner.PublicKey().ToBase58(), - DestinationTokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - ExchangeCurrency: currency_lib.USD, - ExchangeRate: 0.1, - NativeAmount: 100, - Quantity: kin.ToQuarks(1000), - - IsMicroPayment: true, - }, - } - - actionRecords := []*action.Record{ - { - ActionType: action.NoPrivacyTransfer, - Source: tempOutgoingAccount.PublicKey().ToBase58(), - Destination: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - Quantity: pointer.Uint64(kin.ToQuarks(50)), - }, - { - ActionType: action.NoPrivacyTransfer, - Source: tempOutgoingAccount.PublicKey().ToBase58(), - Destination: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - Quantity: pointer.Uint64(kin.ToQuarks(35)), - }, - { - ActionType: action.NoPrivacyTransfer, - Source: tempOutgoingAccount.PublicKey().ToBase58(), - Destination: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - Quantity: pointer.Uint64(kin.ToQuarks(10)), - }, - { - ActionType: action.NoPrivacyTransfer, - Source: tempOutgoingAccount.PublicKey().ToBase58(), - Destination: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - Quantity: pointer.Uint64(kin.ToQuarks(5)), - }, - { - ActionType: action.NoPrivacyWithdraw, - Source: tempOutgoingAccount.PublicKey().ToBase58(), - Destination: &intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount, - Quantity: pointer.Uint64(kin.ToQuarks(900)), - }, - } - - require.NoError(t, env.data.CreateAccountInfo(env.ctx, &account.Record{ - OwnerAccount: intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount, - AuthorityAccount: intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount, - TokenAccount: *actionRecords[1].Destination, - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_PRIMARY, - })) - - require.NoError(t, env.data.CreateAccountInfo(env.ctx, &account.Record{ - OwnerAccount: additionalCodeUserDestinationOwner.PublicKey().ToBase58(), - AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - TokenAccount: *actionRecords[2].Destination, - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_RELATIONSHIP, - RelationshipTo: pointer.String("example.com"), - })) - - originalExchangeData, ok := getExchangeDataFromIntent(intentRecord) - require.True(t, ok) - - exchangeDataByOwner, err := getMicroPaymentReceiveExchangeDataByOwner(env.ctx, env.data, originalExchangeData, intentRecord, actionRecords) - require.NoError(t, err) - require.Len(t, exchangeDataByOwner, 2) - - actualExchangeData, ok := exchangeDataByOwner[micropaymentDestinationOwner.PublicKey().ToBase58()] - require.True(t, ok) - assert.Equal(t, originalExchangeData.Currency, actualExchangeData.Currency) - assert.Equal(t, originalExchangeData.ExchangeRate, actualExchangeData.ExchangeRate) - assert.Equal(t, 93.5, actualExchangeData.NativeAmount) - assert.Equal(t, kin.ToQuarks(935), actualExchangeData.Quarks) - - actualExchangeData, ok = exchangeDataByOwner[additionalCodeUserDestinationOwner.PublicKey().ToBase58()] - require.True(t, ok) - assert.Equal(t, originalExchangeData.Currency, actualExchangeData.Currency) - assert.Equal(t, originalExchangeData.ExchangeRate, actualExchangeData.ExchangeRate) - assert.Equal(t, 1.0, actualExchangeData.NativeAmount) - assert.Equal(t, kin.ToQuarks(10), actualExchangeData.Quarks) -} diff --git a/pkg/code/common/account.go b/pkg/code/common/account.go index faee4d97..b2d1f14f 100644 --- a/pkg/code/common/account.go +++ b/pkg/code/common/account.go @@ -19,10 +19,6 @@ import ( "github.com/code-payments/code-server/pkg/solana/token" ) -var ( - ErrNoPrivacyMigration2022 = errors.New("no privacy migration 2022 for owner") -) - type Account struct { publicKey *Key privateKey *Key // Optional @@ -360,120 +356,8 @@ func (a *TimelockAccounts) GetInitializeInstruction(memory *Account, accountInde ), nil } -// GetTransferWithAuthorityInstruction gets a TransferWithAuthority instruction for a timelock account -func (a *TimelockAccounts) GetTransferWithAuthorityInstruction(destination *Account, quarks uint64) (solana.Instruction, error) { - if err := destination.Validate(); err != nil { - return solana.Instruction{}, err - } - - if quarks == 0 { - return solana.Instruction{}, errors.New("quarks must be positive") - } - - return timelock_token_v1.NewTransferWithAuthorityInstruction( - &timelock_token_v1.TransferWithAuthorityInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - TimeAuthority: GetSubsidizer().publicKey.ToBytes(), - Destination: destination.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.TransferWithAuthorityInstructionArgs{ - TimelockBump: a.StateBump, - Amount: quarks, - }, - ).ToLegacyInstruction(), nil -} - -// GetWithdrawInstruction gets a Withdraw instruction for a timelock account -func (a *TimelockAccounts) GetWithdrawInstruction(destination *Account) (solana.Instruction, error) { - if err := destination.Validate(); err != nil { - return solana.Instruction{}, err - } - - return timelock_token_v1.NewWithdrawInstruction( - &timelock_token_v1.WithdrawInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - Destination: destination.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.WithdrawInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil -} - -// GetBurnDustWithAuthorityInstruction gets a BurnDustWithAuthority instruction for a timelock account -func (a *TimelockAccounts) GetBurnDustWithAuthorityInstruction(maxQuarks uint64) (solana.Instruction, error) { - if maxQuarks == 0 { - return solana.Instruction{}, errors.New("max quarks must be positive") - } - - return timelock_token_v1.NewBurnDustWithAuthorityInstruction( - &timelock_token_v1.BurnDustWithAuthorityInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - TimeAuthority: GetSubsidizer().publicKey.ToBytes(), - Mint: a.Mint.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.BurnDustWithAuthorityInstructionArgs{ - TimelockBump: a.StateBump, - MaxAmount: maxQuarks, - }, - ).ToLegacyInstruction(), nil -} - -// GetRevokeLockWithAuthorityInstruction gets a RevokeLockWithAuthority instruction for a timelock account -func (a *TimelockAccounts) GetRevokeLockWithAuthorityInstruction() (solana.Instruction, error) { - return timelock_token_v1.NewRevokeLockWithAuthorityInstruction( - &timelock_token_v1.RevokeLockWithAuthorityInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - TimeAuthority: GetSubsidizer().publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.RevokeLockWithAuthorityInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil -} - -// GetDeactivateInstruction gets a Deactivate instruction for a timelock account -func (a *TimelockAccounts) GetDeactivateInstruction() (solana.Instruction, error) { - return timelock_token_v1.NewDeactivateInstruction( - &timelock_token_v1.DeactivateInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - VaultOwner: a.VaultOwner.publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.DeactivateInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil -} - -// GetCloseAccountsInstruction gets a CloseAccounts instruction for a timelock account -func (a *TimelockAccounts) GetCloseAccountsInstruction() (solana.Instruction, error) { - return timelock_token_v1.NewCloseAccountsInstruction( - &timelock_token_v1.CloseAccountsInstructionAccounts{ - Timelock: a.State.publicKey.ToBytes(), - Vault: a.Vault.publicKey.ToBytes(), - CloseAuthority: GetSubsidizer().publicKey.ToBytes(), - Payer: GetSubsidizer().publicKey.ToBytes(), - }, - &timelock_token_v1.CloseAccountsInstructionArgs{ - TimelockBump: a.StateBump, - }, - ).ToLegacyInstruction(), nil -} - -// ValidateExternalKinTokenAccount validates an address is an external Kin token account -func ValidateExternalKinTokenAccount(ctx context.Context, data code_data.Provider, tokenAccount *Account) (bool, string, error) { +// ValidateExternalTokenAccount validates an address is an external token account for the core mint +func ValidateExternalTokenAccount(ctx context.Context, data code_data.Provider, tokenAccount *Account) (bool, string, error) { _, err := data.GetBlockchainTokenAccountInfo(ctx, tokenAccount.publicKey.ToBase58(), solana.CommitmentFinalized) switch err { case nil: diff --git a/pkg/code/common/account_test.go b/pkg/code/common/account_test.go index 5e5c6b3c..36a51fa7 100644 --- a/pkg/code/common/account_test.go +++ b/pkg/code/common/account_test.go @@ -12,8 +12,6 @@ import ( commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/cvm" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/solana/token" @@ -247,169 +245,6 @@ func TestGetInitializeInstruction(t *testing.T) { // todo: implement me } -func TestGetTransferWithAuthorityInstruction(t *testing.T) { - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - source, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) - require.NoError(t, err) - - destination := newRandomTestAccount(t) - amount := kin.ToQuarks(123) - - ixn, err := source.GetTransferWithAuthorityInstruction(destination, amount) - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.TransferWithAuthorityInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, source.StateBump, args.TimelockBump) - assert.Equal(t, amount, args.Amount) - - assert.EqualValues(t, source.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, source.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), accounts.VaultOwner) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.TimeAuthority) - assert.EqualValues(t, destination.PublicKey().ToBytes(), accounts.Destination) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetWithdrawInstruction(t *testing.T) { - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - source, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) - require.NoError(t, err) - - destination := newRandomTestAccount(t) - - ixn, err := source.GetWithdrawInstruction(destination) - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.WithdrawInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, source.StateBump, args.TimelockBump) - - assert.EqualValues(t, source.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, source.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), accounts.VaultOwner) - assert.EqualValues(t, destination.PublicKey().ToBytes(), accounts.Destination) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetBurnDustWithAuthorityInstruction(t *testing.T) { - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) - require.NoError(t, err) - - maxAmount := kin.ToQuarks(1) - - ixn, err := timelockAccounts.GetBurnDustWithAuthorityInstruction(maxAmount) - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.BurnDustWithAuthorityInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelockAccounts.StateBump, args.TimelockBump) - assert.Equal(t, maxAmount, args.MaxAmount) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, timelockAccounts.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), accounts.VaultOwner) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.TimeAuthority) - assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), accounts.Mint) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetRevokeLockWithAuthorityInstruction(t *testing.T) { - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) - require.NoError(t, err) - - ixn, err := timelockAccounts.GetRevokeLockWithAuthorityInstruction() - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.RevokeLockWithAuthorityFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelockAccounts.StateBump, args.TimelockBump) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, timelockAccounts.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.TimeAuthority) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetDeactivateInstruction(t *testing.T) { - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) - require.NoError(t, err) - - ixn, err := timelockAccounts.GetDeactivateInstruction() - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.DeactivateInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelockAccounts.StateBump, args.TimelockBump) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), accounts.VaultOwner) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - -func TestGetCloseAccountsInstruction(t *testing.T) { - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) - ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) - require.NoError(t, err) - - ixn, err := timelockAccounts.GetCloseAccountsInstruction() - require.NoError(t, err) - - txn := solana.NewTransaction(subsidizerAccount.PublicKey().ToBytes(), ixn) - - args, accounts, err := timelock_token_v1.CloseAccountsInstructionFromLegacyInstruction(txn, 0) - require.NoError(t, err) - - assert.Equal(t, timelockAccounts.StateBump, args.TimelockBump) - - assert.EqualValues(t, timelockAccounts.State.PublicKey().ToBytes(), accounts.Timelock) - assert.EqualValues(t, timelockAccounts.Vault.PublicKey().ToBytes(), accounts.Vault) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.CloseAuthority) - assert.EqualValues(t, subsidizerAccount.PublicKey().ToBytes(), accounts.Payer) -} - func TestConvertToAssociatedTokenAccount(t *testing.T) { ownerAccount := newRandomTestAccount(t) mintAccount := newRandomTestAccount(t) diff --git a/pkg/code/common/mint.go b/pkg/code/common/mint.go index 46078a57..5badd516 100644 --- a/pkg/code/common/mint.go +++ b/pkg/code/common/mint.go @@ -1,11 +1,61 @@ package common import ( - "github.com/code-payments/code-server/pkg/kin" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + + "github.com/code-payments/code-server/pkg/code/config" "github.com/code-payments/code-server/pkg/usdc" ) var ( - KinMintAccount, _ = NewAccountFromPublicKeyBytes(kin.TokenMint) + CoreMintAccount, _ = NewAccountFromPublicKeyBytes(config.CoreMintPublicKeyBytes) + CoreMintQuarksPerUnit = uint64(config.CoreMintQuarksPerUnit) + CoreMintSymbol = config.CoreMintSymbol + CoreMintDecimals = config.CoreMintDecimals + UsdcMintAccount, _ = NewAccountFromPublicKeyBytes(usdc.TokenMint) ) + +func FromCoreMintQuarks(quarks uint64) uint64 { + return quarks / CoreMintQuarksPerUnit +} + +func ToCoreMintQuarks(units uint64) uint64 { + return units * CoreMintQuarksPerUnit +} + +// todo: this needs tests +func StrToQuarks(val string) (int64, error) { + parts := strings.Split(val, ".") + if len(parts) > 2 { + return 0, errors.New("invalid value") + } + + if len(parts[0]) > 19-CoreMintDecimals { + return 0, errors.New("value cannot be represented") + } + + wholeUnits, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return 0, err + } + + var quarks uint64 + if len(parts) == 2 { + if len(parts[1]) > CoreMintDecimals { + return 0, errors.New("value cannot be represented") + } + + padded := fmt.Sprintf("%s%s", parts[1], strings.Repeat("0", CoreMintDecimals-len(parts[1]))) + quarks, err = strconv.ParseUint(padded, 10, 64) + if err != nil { + return 0, errors.Wrap(err, "invalid decimal component") + } + } + + return int64(wholeUnits)*int64(CoreMintQuarksPerUnit) + int64(quarks), nil +} diff --git a/pkg/code/common/mint_test.go b/pkg/code/common/mint_test.go new file mode 100644 index 00000000..31cc11e3 --- /dev/null +++ b/pkg/code/common/mint_test.go @@ -0,0 +1,37 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStrToQuarks_HappyPath(t *testing.T) { + for _, tc := range []struct { + input string + expected int64 + }{ + {"123.456", 123456000}, + {"123", 123000000}, + {"0.456", 456000}, + {"1234567890123.123456", 1234567890123123456}, + } { + quarks, err := StrToQuarks(tc.input) + require.NoError(t, err) + assert.Equal(t, tc.expected, quarks) + } +} + +func TestStrToQuarks_InvalidString(t *testing.T) { + for _, tc := range []string{ + "", + "abc", + "1.1.1", + "0.1234567", + "12345678901234", + } { + _, err := StrToQuarks(tc) + assert.Error(t, err) + } +} diff --git a/pkg/code/common/owner.go b/pkg/code/common/owner.go index 2ef1b89c..b8529137 100644 --- a/pkg/code/common/owner.go +++ b/pkg/code/common/owner.go @@ -9,7 +9,6 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/phone" "github.com/code-payments/code-server/pkg/code/data/timelock" ) @@ -35,10 +34,9 @@ const ( ) type OwnerMetadata struct { - Type OwnerType - Account *Account - VerificationRecord *phone.Verification - State OwnerManagementState + Type OwnerType + Account *Account + State OwnerManagementState } // GetOwnerMetadata gets metadata about an owner account @@ -56,15 +54,11 @@ func GetOwnerMetadata(ctx context.Context, data code_data.Provider, owner *Accou } if mtdt.Type == OwnerTypeUnknown { - // Is the owner account a user's 12 words that's phone verified? - // - // This should be the last thing checked, since it's technically possible - // today for a malicious user to phone very any owner account type. - verificationRecord, err := data.GetLatestPhoneVerificationForAccount(ctx, owner.publicKey.ToBase58()) + // Is the owner account a user 12 words? + _, err := data.GetLatestAccountInfoByOwnerAddressAndType(ctx, owner.publicKey.ToBase58(), commonpb.AccountType_PRIMARY) if err == nil { mtdt.Type = OwnerTypeUser12Words - mtdt.VerificationRecord = verificationRecord - } else if err != phone.ErrVerificationNotFound { + } else if err != account.ErrAccountInfoNotFound { return nil, err } } diff --git a/pkg/code/common/owner_test.go b/pkg/code/common/owner_test.go index be842acc..c1c38c18 100644 --- a/pkg/code/common/owner_test.go +++ b/pkg/code/common/owner_test.go @@ -3,7 +3,6 @@ package common import ( "context" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,7 +11,6 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/phone" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) @@ -30,26 +28,6 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { _, err := GetOwnerMetadata(ctx, data, owner) assert.Equal(t, ErrOwnerNotFound, err) - // Initially phone verified, but OpenAccounts intent not created. Until an - // account type is mapped, we assume a user 12 words, since that's the expected - // path. - - verificationRecord := &phone.Verification{ - PhoneNumber: "+12223334444", - OwnerAccount: owner.PublicKey().ToBase58(), - LastVerifiedAt: time.Now(), - CreatedAt: time.Now(), - } - require.NoError(t, data.SavePhoneVerification(ctx, verificationRecord)) - - actual, err := GetOwnerMetadata(ctx, data, owner) - require.NoError(t, err) - assert.Equal(t, actual.Account.PublicKey().ToBase58(), owner.PublicKey().ToBase58()) - assert.Equal(t, OwnerTypeUser12Words, actual.Type) - assert.Equal(t, OwnerManagementStateNotFound, actual.State) - require.NotNil(t, actual.VerificationRecord) - assert.Equal(t, verificationRecord.PhoneNumber, actual.VerificationRecord.PhoneNumber) - // Later calls intent to OpenAccounts timelockAccounts, err := owner.GetTimelockAccounts(vmAccount, coreMintAccount) @@ -67,13 +45,11 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { } require.NoError(t, data.CreateAccountInfo(ctx, primaryAccountInfoRecord)) - actual, err = GetOwnerMetadata(ctx, data, owner) + actual, err := GetOwnerMetadata(ctx, data, owner) require.NoError(t, err) assert.Equal(t, actual.Account.PublicKey().ToBase58(), owner.PublicKey().ToBase58()) assert.Equal(t, OwnerTypeUser12Words, actual.Type) assert.Equal(t, OwnerManagementStateCodeAccount, actual.State) - require.NotNil(t, actual.VerificationRecord) - assert.Equal(t, verificationRecord.PhoneNumber, actual.VerificationRecord.PhoneNumber) // Add swap account @@ -93,8 +69,6 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { assert.Equal(t, actual.Account.PublicKey().ToBase58(), owner.PublicKey().ToBase58()) assert.Equal(t, OwnerTypeUser12Words, actual.Type) assert.Equal(t, OwnerManagementStateCodeAccount, actual.State) - require.NotNil(t, actual.VerificationRecord) - assert.Equal(t, verificationRecord.PhoneNumber, actual.VerificationRecord.PhoneNumber) // Unlock a Timelock account @@ -107,8 +81,6 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { assert.Equal(t, actual.Account.PublicKey().ToBase58(), owner.PublicKey().ToBase58()) assert.Equal(t, OwnerTypeUser12Words, actual.Type) assert.Equal(t, OwnerManagementStateUnlocked, actual.State) - require.NotNil(t, actual.VerificationRecord) - assert.Equal(t, verificationRecord.PhoneNumber, actual.VerificationRecord.PhoneNumber) } func TestGetOwnerMetadata_RemoteSendGiftCard(t *testing.T) { @@ -123,16 +95,6 @@ func TestGetOwnerMetadata_RemoteSendGiftCard(t *testing.T) { _, err := GetOwnerMetadata(ctx, data, owner) assert.Equal(t, ErrOwnerNotFound, err) - // It's possible a malicious user could phone verify a gift card owner, which - // we should ignore - verificationRecord := &phone.Verification{ - PhoneNumber: "+12223334444", - OwnerAccount: owner.PublicKey().ToBase58(), - LastVerifiedAt: time.Now(), - CreatedAt: time.Now(), - } - require.NoError(t, data.SavePhoneVerification(ctx, verificationRecord)) - timelockAccounts, err := owner.GetTimelockAccounts(vmAccount, mintAccount) require.NoError(t, err) @@ -153,7 +115,6 @@ func TestGetOwnerMetadata_RemoteSendGiftCard(t *testing.T) { assert.Equal(t, actual.Account.PublicKey().ToBase58(), owner.PublicKey().ToBase58()) assert.Equal(t, OwnerTypeRemoteSendGiftCard, actual.Type) assert.Equal(t, OwnerManagementStateCodeAccount, actual.State) - assert.Nil(t, actual.VerificationRecord) } func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { diff --git a/pkg/code/common/subsidizer.go b/pkg/code/common/subsidizer.go index 19ce796a..e57b6b49 100644 --- a/pkg/code/common/subsidizer.go +++ b/pkg/code/common/subsidizer.go @@ -7,6 +7,7 @@ import ( "github.com/newrelic/go-agent/v3/newrelic" + "github.com/code-payments/code-server/pkg/code/config" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/nonce" @@ -19,15 +20,13 @@ import ( const ( // Important Note: Be very careful changing this value, as it will completely // change timelock PDAs and have consequences with existing splitter treasuries. - // - // todo: configurable - realSubsidizerPublicKey = "codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR" + realSubsidizerPublicKey = config.SubsidizerPublicKey // Ensure this is a large enough buffer. The enforcement of a min balance isn't // perfect to say the least. // // todo: configurable - minSubsidizerBalance = 250_000_000_000 // 250 SOL + minSubsidizerBalance = 10_000_000_000 // 10 SOL ) var ( @@ -36,20 +35,10 @@ var ( // and SOL balances for our subsidizer, so we exclude rent recovery which // ensures our estimates are always on the conservative side of things. lamportsByFulfillment = map[fulfillment.Type]uint64{ - fulfillment.InitializeLockedTimelockAccount: 4120000, // 0.00412 SOL - fulfillment.NoPrivacyTransferWithAuthority: 10000, // 0.00001 SOL (5000 lamports per signature) - fulfillment.NoPrivacyWithdraw: 10000, // 0.00001 SOL (5000 lamports per signature) - fulfillment.TemporaryPrivacyTransferWithAuthority: 10000, // 0.00001 SOL (5000 lamports per signature) - fulfillment.PermanentPrivacyTransferWithAuthority: 10000, // 0.00001 SOL (5000 lamports per signature) - fulfillment.TransferWithCommitment: 5000, // 0.000005 SOL (5000 lamports per signature) - fulfillment.CloseEmptyTimelockAccount: 10000, // 0.00001 SOL (5000 lamports per signature) - fulfillment.CloseDormantTimelockAccount: 10000, // 0.00001 SOL (5000 lamports per signature) - fulfillment.SaveRecentRoot: 5000, // 0.000005 SOL (5000 lamports per signature) - fulfillment.InitializeCommitmentProof: 15800000, // 0.0158 SOL - fulfillment.UploadCommitmentProof: 5000, // 0.000005 SOL (5000 lamports per signature) - fulfillment.VerifyCommitmentProof: 5000, // 0.000005 SOL (5000 lamports per signature) - fulfillment.OpenCommitmentVault: 2050000, // 0.00205 SOL - fulfillment.CloseCommitment: 5000, // 0.000005 SOL (5000 lamports per signature) + fulfillment.InitializeLockedTimelockAccount: 5000, // 0.000005 SOL (5000 lamports per signature) + fulfillment.NoPrivacyTransferWithAuthority: 5000, // 0.000005 SOL (5000 lamports per signature) + fulfillment.NoPrivacyWithdraw: 5000, // 0.000005 SOL (5000 lamports per signature) + fulfillment.CloseEmptyTimelockAccount: 5000, // 0.000005 SOL (5000 lamports per signature) } lamportsPerCreateNonceAccount uint64 = 1450000 // 0.00145 SOL ) diff --git a/pkg/code/common/subsidizer_test.go b/pkg/code/common/subsidizer_test.go index 878dfe1a..ae8d5003 100644 --- a/pkg/code/common/subsidizer_test.go +++ b/pkg/code/common/subsidizer_test.go @@ -21,14 +21,14 @@ func TestEstimateUsedSubsidizerBalance(t *testing.T) { fulfillmentRecords := []*fulfillment.Record{ // These records are included in fee calculation - {IntentType: intent.SendPrivatePayment, ActionType: action.PrivateTransfer, FulfillmentType: fulfillment.PermanentPrivacyTransferWithAuthority, State: fulfillment.StatePending, Intent: "i1", Data: []byte("txn"), Nonce: pointer.String("n1"), Blockhash: pointer.String("bh1"), Signature: pointer.String("s1"), Source: "source", Destination: pointer.String("destination")}, - {IntentType: intent.ReceivePaymentsPrivately, ActionType: action.PrivateTransfer, FulfillmentType: fulfillment.PermanentPrivacyTransferWithAuthority, State: fulfillment.StatePending, Intent: "i2", Data: []byte("txn"), Nonce: pointer.String("n2"), Blockhash: pointer.String("bh2"), Signature: pointer.String("s2"), Source: "source", Destination: pointer.String("destination")}, + {IntentType: intent.SendPublicPayment, ActionType: action.NoPrivacyTransfer, FulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, State: fulfillment.StatePending, Intent: "i1", Data: []byte("txn"), Nonce: pointer.String("n1"), Blockhash: pointer.String("bh1"), Signature: pointer.String("s1"), Source: "source", Destination: pointer.String("destination")}, + {IntentType: intent.SendPublicPayment, ActionType: action.NoPrivacyTransfer, FulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, State: fulfillment.StatePending, Intent: "i2", Data: []byte("txn"), Nonce: pointer.String("n2"), Blockhash: pointer.String("bh2"), Signature: pointer.String("s2"), Source: "source", Destination: pointer.String("destination")}, {IntentType: intent.OpenAccounts, ActionType: action.OpenAccount, FulfillmentType: fulfillment.InitializeLockedTimelockAccount, State: fulfillment.StatePending, Intent: "i3", Data: []byte("txn"), Nonce: pointer.String("n3"), Blockhash: pointer.String("bh3"), Signature: pointer.String("s3"), Source: "source"}, // These records aren't included in fee calculation - {IntentType: intent.SendPrivatePayment, ActionType: action.PrivateTransfer, FulfillmentType: fulfillment.PermanentPrivacyTransferWithAuthority, State: fulfillment.StateUnknown, Intent: "i4", Data: []byte("txn"), Nonce: pointer.String("n4"), Blockhash: pointer.String("bh4"), Signature: pointer.String("s4"), Source: "source", Destination: pointer.String("destination")}, - {IntentType: intent.SendPrivatePayment, ActionType: action.PrivateTransfer, FulfillmentType: fulfillment.PermanentPrivacyTransferWithAuthority, State: fulfillment.StateFailed, Intent: "i5", Data: []byte("txn"), Nonce: pointer.String("n5"), Blockhash: pointer.String("bh5"), Signature: pointer.String("s5"), Source: "source", Destination: pointer.String("destination")}, - {IntentType: intent.ReceivePaymentsPrivately, ActionType: action.PrivateTransfer, FulfillmentType: fulfillment.PermanentPrivacyTransferWithAuthority, State: fulfillment.StateRevoked, Intent: "i6", Data: []byte("txn"), Nonce: pointer.String("n6"), Blockhash: pointer.String("bh6"), Signature: pointer.String("s6"), Source: "source", Destination: pointer.String("destination")}, + {IntentType: intent.SendPublicPayment, ActionType: action.NoPrivacyTransfer, FulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, State: fulfillment.StateUnknown, Intent: "i4", Data: []byte("txn"), Nonce: pointer.String("n4"), Blockhash: pointer.String("bh4"), Signature: pointer.String("s4"), Source: "source", Destination: pointer.String("destination")}, + {IntentType: intent.SendPublicPayment, ActionType: action.NoPrivacyTransfer, FulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, State: fulfillment.StateFailed, Intent: "i5", Data: []byte("txn"), Nonce: pointer.String("n5"), Blockhash: pointer.String("bh5"), Signature: pointer.String("s5"), Source: "source", Destination: pointer.String("destination")}, + {IntentType: intent.SendPublicPayment, ActionType: action.NoPrivacyTransfer, FulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, State: fulfillment.StateRevoked, Intent: "i6", Data: []byte("txn"), Nonce: pointer.String("n6"), Blockhash: pointer.String("bh6"), Signature: pointer.String("s6"), Source: "source", Destination: pointer.String("destination")}, {IntentType: intent.OpenAccounts, ActionType: action.OpenAccount, FulfillmentType: fulfillment.InitializeLockedTimelockAccount, State: fulfillment.StateConfirmed, Intent: "i7", Data: []byte("txn"), Nonce: pointer.String("n7"), Blockhash: pointer.String("bh7"), Signature: pointer.String("s7"), Source: "source"}, } require.NoError(t, data.PutAllFulfillments(ctx, fulfillmentRecords...)) @@ -56,7 +56,7 @@ func TestEstimateUsedSubsidizerBalance(t *testing.T) { require.NoError(t, err) assert.EqualValues( t, - 3*lamportsPerCreateNonceAccount+2*lamportsByFulfillment[fulfillment.PermanentPrivacyTransferWithAuthority]+lamportsByFulfillment[fulfillment.InitializeLockedTimelockAccount], + 3*lamportsPerCreateNonceAccount+2*lamportsByFulfillment[fulfillment.NoPrivacyTransferWithAuthority]+lamportsByFulfillment[fulfillment.InitializeLockedTimelockAccount], fees, ) } diff --git a/pkg/code/common/vm.go b/pkg/code/common/vm.go index 37f29918..9c3a1147 100644 --- a/pkg/code/common/vm.go +++ b/pkg/code/common/vm.go @@ -1,13 +1,11 @@ package common +import "github.com/code-payments/code-server/pkg/code/config" + var ( - // The well-known Code VM instance used by the Code app - // - // todo: real public key once program is deployed and VM instance is initialized - CodeVmAccount, _ = NewAccountFromPublicKeyString("BkwoMG33cgSDrc3fEjfhZufqzYC3icXTTMajuueXyYGG") + // The well-known Code VM instance + CodeVmAccount, _ = NewAccountFromPublicKeyString(config.VmAccountPublicKey) - // The well-known Code VM instance omnibus used by the Code app - // - // todo: real public key once program is deployed and VM omnibus instance is initialized - CodeVmOmnibusAccount, _ = NewAccountFromPublicKeyString("SqKpQBYg8H69c8dusmSoLsya281cqhfzDVR2EJFHy1P") + // The well-known Code VM instance omnibus account + CodeVmOmnibusAccount, _ = NewAccountFromPublicKeyString(config.VmOmnibusPublicKey) ) diff --git a/pkg/code/config/config.go b/pkg/code/config/config.go new file mode 100644 index 00000000..052849b8 --- /dev/null +++ b/pkg/code/config/config.go @@ -0,0 +1,38 @@ +package config + +import ( + "github.com/mr-tron/base58" + + currency_lib "github.com/code-payments/code-server/pkg/currency" + "github.com/code-payments/code-server/pkg/usdc" +) + +// todo: more things can be pulled into here to configure the open code protocol +// todo: make these environment configs + +const ( + // Random values. Replace with real mint configuration + CoreMintPublicKeyString = "DWYE8SQkpestTvpCxGNTCRjC2E9Kn6TCnu2SxkddrEEU" + CoreMintQuarksPerUnit = uint64(usdc.QuarksPerUsdc) + CoreMintSymbol = currency_lib.USDC + CoreMintDecimals = usdc.Decimals + + // Random value. Replace with real subsidizer public keys + SubsidizerPublicKey = "84ydcM4Yp59W6aZP6eSaKiAMaKidNLfb5k318sT2pm14" + + // Random value. Replace with real VM public keys + VmAccountPublicKey = "BVMGLfRgr3nVFCH5DuW6VR2kfSDxq4EFEopXfwCDpYzb" + VmOmnibusPublicKey = "GNw1t85VH8b1CcwB5933KBC7PboDPJ5EcQdGynbfN1Pb" +) + +var ( + CoreMintPublicKeyBytes []byte +) + +func init() { + decoded, err := base58.Decode(CoreMintPublicKeyString) + if err != nil { + panic(err) + } + CoreMintPublicKeyBytes = decoded +} diff --git a/pkg/code/data/action/action.go b/pkg/code/data/action/action.go index 166f0993..1e355641 100644 --- a/pkg/code/data/action/action.go +++ b/pkg/code/data/action/action.go @@ -17,8 +17,8 @@ const ( CloseDormantAccount // Deprecated by the VM NoPrivacyTransfer NoPrivacyWithdraw - PrivateTransfer // Incorprorates all client-side private movement of funds. Backend processes don't care about the distinction, yet. - SaveRecentRoot + PrivateTransfer // Deprecated privacy flow + SaveRecentRoot // Deprecated privacy flow ) type State uint8 @@ -60,8 +60,6 @@ type Record struct { // use cases before forming a firm opinion. Quantity *uint64 - InitiatorPhoneNumber *string - State State CreatedAt time.Time @@ -94,10 +92,6 @@ func (r *Record) Validate() error { return errors.New("quantity is required when set") } - if r.InitiatorPhoneNumber != nil && len(*r.InitiatorPhoneNumber) == 0 { - return errors.New("initiator phone number is required when set") - } - return nil } @@ -115,8 +109,6 @@ func (r *Record) Clone() Record { Destination: pointer.StringCopy(r.Destination), Quantity: pointer.Uint64Copy(r.Quantity), - InitiatorPhoneNumber: pointer.StringCopy(r.InitiatorPhoneNumber), - State: r.State, CreatedAt: r.CreatedAt, @@ -136,8 +128,6 @@ func (r *Record) CopyTo(dst *Record) { dst.Destination = r.Destination dst.Quantity = r.Quantity - dst.InitiatorPhoneNumber = r.InitiatorPhoneNumber - dst.State = r.State dst.CreatedAt = r.CreatedAt diff --git a/pkg/code/data/action/memory/store.go b/pkg/code/data/action/memory/store.go index ef3352e9..9200b270 100644 --- a/pkg/code/data/action/memory/store.go +++ b/pkg/code/data/action/memory/store.go @@ -7,9 +7,9 @@ import ( "sync" "time" + "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/action" ) type ById []*action.Record diff --git a/pkg/code/data/action/postgres/model.go b/pkg/code/data/action/postgres/model.go index 88a084da..85d47d18 100644 --- a/pkg/code/data/action/postgres/model.go +++ b/pkg/code/data/action/postgres/model.go @@ -9,9 +9,9 @@ import ( "github.com/jmoiron/sqlx" - pgutil "github.com/code-payments/code-server/pkg/database/postgres" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/intent" + pgutil "github.com/code-payments/code-server/pkg/database/postgres" ) const ( @@ -19,17 +19,16 @@ const ( ) type model struct { - Id sql.NullInt64 `db:"id"` - Intent string `db:"intent"` - IntentType uint `db:"intent_type"` - ActionId uint `db:"action_id"` - ActionType uint `db:"action_type"` - Source string `db:"source"` - Destination sql.NullString `db:"destination"` - Quantity sql.NullInt64 `db:"quantity"` - InitiatorPhoneNumber sql.NullString `db:"initiator_phone_number"` - State uint `db:"state"` - CreatedAt time.Time `db:"created_at"` + Id sql.NullInt64 `db:"id"` + Intent string `db:"intent"` + IntentType uint `db:"intent_type"` + ActionId uint `db:"action_id"` + ActionType uint `db:"action_type"` + Source string `db:"source"` + Destination sql.NullString `db:"destination"` + Quantity sql.NullInt64 `db:"quantity"` + State uint `db:"state"` + CreatedAt time.Time `db:"created_at"` } func toModel(obj *action.Record) (*model, error) { @@ -49,23 +48,16 @@ func toModel(obj *action.Record) (*model, error) { quantity.Int64 = int64(*obj.Quantity) } - var initiatorPhoneNumber sql.NullString - if obj.InitiatorPhoneNumber != nil { - initiatorPhoneNumber.Valid = true - initiatorPhoneNumber.String = *obj.InitiatorPhoneNumber - } - return &model{ - Intent: obj.Intent, - IntentType: uint(obj.IntentType), - ActionId: uint(obj.ActionId), - ActionType: uint(obj.ActionType), - Source: obj.Source, - Destination: destination, - Quantity: quantity, - InitiatorPhoneNumber: initiatorPhoneNumber, - State: uint(obj.State), - CreatedAt: obj.CreatedAt, + Intent: obj.Intent, + IntentType: uint(obj.IntentType), + ActionId: uint(obj.ActionId), + ActionType: uint(obj.ActionType), + Source: obj.Source, + Destination: destination, + Quantity: quantity, + State: uint(obj.State), + CreatedAt: obj.CreatedAt, }, nil } @@ -81,23 +73,17 @@ func fromModel(obj *model) *action.Record { quantity = &value } - var initiatorPhoneNumber *string - if obj.InitiatorPhoneNumber.Valid { - initiatorPhoneNumber = &obj.InitiatorPhoneNumber.String - } - return &action.Record{ - Id: uint64(obj.Id.Int64), - Intent: obj.Intent, - IntentType: intent.Type(obj.IntentType), - ActionId: uint32(obj.ActionId), - ActionType: action.Type(obj.ActionType), - Source: obj.Source, - Destination: destination, - Quantity: quantity, - InitiatorPhoneNumber: initiatorPhoneNumber, - State: action.State(obj.State), - CreatedAt: obj.CreatedAt, + Id: uint64(obj.Id.Int64), + Intent: obj.Intent, + IntentType: intent.Type(obj.IntentType), + ActionId: uint32(obj.ActionId), + ActionType: action.Type(obj.ActionType), + Source: obj.Source, + Destination: destination, + Quantity: quantity, + State: action.State(obj.State), + CreatedAt: obj.CreatedAt, } } @@ -118,7 +104,7 @@ func (m *model) dbUpdate(ctx context.Context, db *sqlx.DB) error { query := fmt.Sprintf(`UPDATE `+tableName+` SET state = $3%s WHERE intent = $1 AND action_id = $2 - RETURNING id, intent, intent_type, action_id, action_type, source, destination, quantity, initiator_phone_number, state, created_at + RETURNING id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at `, quantityUpdateStmt) err := tx.QueryRowxContext( @@ -137,7 +123,7 @@ func (m *model) dbUpdate(ctx context.Context, db *sqlx.DB) error { func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*model) ([]*model, error) { var res []*model - query := `INSERT INTO ` + tableName + ` (intent, intent_type, action_id, action_type, source, destination, quantity, initiator_phone_number, state, created_at) VALUES ` + query := `INSERT INTO ` + tableName + ` (intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at) VALUES ` var parameters []interface{} for i, model := range models { @@ -147,8 +133,8 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*model) ([]*model, baseIndex := len(parameters) query += fmt.Sprintf( - `($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d)`, - baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7, baseIndex+8, baseIndex+9, baseIndex+10, + `($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d)`, + baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7, baseIndex+8, baseIndex+9, ) if i != len(models)-1 { @@ -164,13 +150,12 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*model) ([]*model, model.Source, model.Destination, model.Quantity, - model.InitiatorPhoneNumber, model.State, model.CreatedAt, ) } - query += ` RETURNING id, intent, intent_type, action_id, action_type, source, destination, quantity, initiator_phone_number, state, created_at` + query += ` RETURNING id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at` err := tx.SelectContext( ctx, @@ -188,7 +173,7 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*model) ([]*model, func dbGetById(ctx context.Context, db *sqlx.DB, intent string, actionId uint32) (*model, error) { res := &model{} - query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, initiator_phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at FROM ` + tableName + ` WHERE intent = $1 AND action_id = $2 LIMIT 1` @@ -203,7 +188,7 @@ func dbGetById(ctx context.Context, db *sqlx.DB, intent string, actionId uint32) func dbGetAllByIntent(ctx context.Context, db *sqlx.DB, intent string) ([]*model, error) { res := []*model{} - query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, initiator_phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at FROM ` + tableName + ` WHERE intent = $1 ORDER BY action_id ASC` @@ -223,7 +208,7 @@ func dbGetAllByIntent(ctx context.Context, db *sqlx.DB, intent string) ([]*model func dbGetAllByAddress(ctx context.Context, db *sqlx.DB, address string) ([]*model, error) { res := []*model{} - query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, initiator_phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at FROM ` + tableName + ` WHERE source = $1 OR destination = $1` @@ -312,7 +297,7 @@ func dbGetNetBalanceBatch(ctx context.Context, db *sqlx.DB, accounts ...string) func dbGetGiftCardClaimedAction(ctx context.Context, db *sqlx.DB, giftCardVault string) (*model, error) { res := []*model{} - query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, initiator_phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at FROM ` + tableName + ` WHERE source = $1 AND action_type = $2 AND state != $3 LIMIT 2` @@ -341,7 +326,7 @@ func dbGetGiftCardClaimedAction(ctx context.Context, db *sqlx.DB, giftCardVault func dbGetGiftCardAutoReturnAction(ctx context.Context, db *sqlx.DB, giftCardVault string) (*model, error) { res := []*model{} - query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, initiator_phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, source, destination, quantity, state, created_at FROM ` + tableName + ` WHERE source = $1 AND action_type = $2 AND state != $3 LIMIT 2` diff --git a/pkg/code/data/action/postgres/store.go b/pkg/code/data/action/postgres/store.go index 446f0ca0..c841300e 100644 --- a/pkg/code/data/action/postgres/store.go +++ b/pkg/code/data/action/postgres/store.go @@ -8,8 +8,8 @@ import ( "github.com/jmoiron/sqlx" - pgutil "github.com/code-payments/code-server/pkg/database/postgres" "github.com/code-payments/code-server/pkg/code/data/action" + pgutil "github.com/code-payments/code-server/pkg/database/postgres" ) type store struct { diff --git a/pkg/code/data/action/postgres/store_test.go b/pkg/code/data/action/postgres/store_test.go index 82c34565..86758484 100644 --- a/pkg/code/data/action/postgres/store_test.go +++ b/pkg/code/data/action/postgres/store_test.go @@ -32,8 +32,6 @@ const ( destination TEXT NULL, quantity INTEGER NULL, - initiator_phone_number TEXT NULL, - state INTEGER NOT NULL, created_at timestamp with time zone NOT NULL, diff --git a/pkg/code/data/action/tests/tests.go b/pkg/code/data/action/tests/tests.go index eea08a8b..3103ff47 100644 --- a/pkg/code/data/action/tests/tests.go +++ b/pkg/code/data/action/tests/tests.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/pointer" ) func RunTests(t *testing.T, s action.Store, teardown func()) { @@ -47,8 +47,6 @@ func testRoundTrip(t *testing.T, s action.Store) { Destination: pointer.String("destination"), Quantity: nil, - InitiatorPhoneNumber: pointer.String("phone"), - State: action.StateConfirmed, } @@ -384,7 +382,5 @@ func assertEquivalentRecords(t *testing.T, obj1, obj2 *action.Record) { assert.EqualValues(t, obj1.Destination, obj2.Destination) assert.EqualValues(t, obj1.Quantity, obj2.Quantity) - assert.EqualValues(t, obj1.InitiatorPhoneNumber, obj2.InitiatorPhoneNumber) - assert.Equal(t, obj1.State, obj2.State) } diff --git a/pkg/code/data/airdrop/memory/store.go b/pkg/code/data/airdrop/memory/store.go deleted file mode 100644 index 924ad156..00000000 --- a/pkg/code/data/airdrop/memory/store.go +++ /dev/null @@ -1,52 +0,0 @@ -package memory - -import ( - "context" - "sync" - - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/code/data/airdrop" -) - -type store struct { - mu sync.Mutex - ineligibleAidropOwners map[string]any -} - -// New returns a new in memory airdrop.Store -func New() airdrop.Store { - return &store{ - ineligibleAidropOwners: make(map[string]any), - } -} - -// MarkIneligible implements airdrop.Store.MarkIneligible -func (s *store) MarkIneligible(ctx context.Context, owner string) error { - if len(owner) == 0 { - return errors.New("owner is required") - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.ineligibleAidropOwners[owner] = struct{}{} - - return nil -} - -// IsEligible implements airdrop.Store.IsEligible -func (s *store) IsEligible(ctx context.Context, owner string) (bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - - _, isIneligible := s.ineligibleAidropOwners[owner] - return !isIneligible, nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.ineligibleAidropOwners = make(map[string]any) -} diff --git a/pkg/code/data/airdrop/memory/store_test.go b/pkg/code/data/airdrop/memory/store_test.go deleted file mode 100644 index 88d99419..00000000 --- a/pkg/code/data/airdrop/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/airdrop/tests" -) - -func TestAirdropMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/airdrop/postgres/model.go b/pkg/code/data/airdrop/postgres/model.go deleted file mode 100644 index ba265899..00000000 --- a/pkg/code/data/airdrop/postgres/model.go +++ /dev/null @@ -1,80 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "errors" - "time" - - "github.com/jmoiron/sqlx" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" -) - -const ( - tableName = "codewallet__core_airdropeligibility" -) - -var ( - errNotFound = errors.New("airdrop eligibility record not found") -) - -type model struct { - Id sql.NullInt64 `db:"id"` - - Owner string `db:"owner"` - IsEligible bool `db:"is_eligible"` - - CreatedAt time.Time `db:"created_at"` -} - -func toIneligibleModel(owner string) (*model, error) { - if len(owner) == 0 { - return nil, errors.New("owner is required") - } - - return &model{ - Owner: owner, - IsEligible: false, - CreatedAt: time.Now(), - }, nil -} - -func (m *model) dbPut(ctx context.Context, db *sqlx.DB) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `INSERT INTO ` + tableName + ` - (owner, is_eligible, created_at) - VALUES ($1, $2, $3) - - ON CONFLICT (owner) - DO UPDATE - SET is_eligible = $2 - WHERE ` + tableName + `.owner = $1 - - RETURNING id, owner, is_eligible, created_at` - - return tx.QueryRowxContext( - ctx, - query, - m.Owner, - m.IsEligible, - m.CreatedAt.UTC(), - ).StructScan(m) - }) -} - -func dbGet(ctx context.Context, db *sqlx.DB, owner string) (*model, error) { - res := &model{} - - query := `SELECT - id, owner, is_eligible, created_at - FROM ` + tableName + ` - WHERE owner = $1 - LIMIT 1` - - err := db.GetContext(ctx, res, query, owner) - if err != nil { - return nil, pgutil.CheckNoRows(err, errNotFound) - } - return res, nil -} diff --git a/pkg/code/data/airdrop/postgres/store.go b/pkg/code/data/airdrop/postgres/store.go deleted file mode 100644 index 8ddbbd0f..00000000 --- a/pkg/code/data/airdrop/postgres/store.go +++ /dev/null @@ -1,42 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/airdrop" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres airdrop.Store -func New(db *sql.DB) airdrop.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// MarkIneligible implements airdrop.Store.MarkIneligible -func (s *store) MarkIneligible(ctx context.Context, owner string) error { - model, err := toIneligibleModel(owner) - if err != nil { - return err - } - - return model.dbPut(ctx, s.db) -} - -// IsEligible implements airdrop.Store.IsEligible -func (s *store) IsEligible(ctx context.Context, owner string) (bool, error) { - model, err := dbGet(ctx, s.db, owner) - if err == errNotFound { - // Backwards compatibility with a default true value - return true, nil - } - - return model.IsEligible, nil -} diff --git a/pkg/code/data/airdrop/postgres/store_test.go b/pkg/code/data/airdrop/postgres/store_test.go deleted file mode 100644 index 16814d2c..00000000 --- a/pkg/code/data/airdrop/postgres/store_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/airdrop" - "github.com/code-payments/code-server/pkg/code/data/airdrop/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -var ( - testStore airdrop.Store - teardown func() -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_airdropeligibility ( - id SERIAL NOT NULL PRIMARY KEY, - - owner TEXT NOT NULL, - is_eligible BOOL NOT NULL, - - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_airdropeligibility__uniq__owner UNIQUE (owner) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_airdropeligibility; - ` -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestAirdropPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/airdrop/store.go b/pkg/code/data/airdrop/store.go deleted file mode 100644 index 785817aa..00000000 --- a/pkg/code/data/airdrop/store.go +++ /dev/null @@ -1,11 +0,0 @@ -package airdrop - -import "context" - -type Store interface { - // MarkIneligible marks an owner account as being ineligible for aidrop - MarkIneligible(ctx context.Context, owner string) error - - // IsEligibile returns whether an owner account is eligible for airdrops - IsEligible(ctx context.Context, owner string) (bool, error) -} diff --git a/pkg/code/data/airdrop/tests/tests.go b/pkg/code/data/airdrop/tests/tests.go deleted file mode 100644 index 7f59a47b..00000000 --- a/pkg/code/data/airdrop/tests/tests.go +++ /dev/null @@ -1,38 +0,0 @@ -package tests - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/airdrop" -) - -func RunTests(t *testing.T, s airdrop.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s airdrop.Store){ - testHappyPath, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s airdrop.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - isEligible, err := s.IsEligible(ctx, "owner") - require.NoError(t, err) - assert.True(t, isEligible) - - for i := 0; i < 3; i++ { - require.NoError(t, s.MarkIneligible(ctx, "owner")) - - isEligible, err = s.IsEligible(ctx, "owner") - require.NoError(t, err) - assert.False(t, isEligible) - } - }) -} diff --git a/pkg/code/data/badgecount/badge_count.go b/pkg/code/data/badgecount/badge_count.go deleted file mode 100644 index f32fd279..00000000 --- a/pkg/code/data/badgecount/badge_count.go +++ /dev/null @@ -1,46 +0,0 @@ -package badgecount - -import ( - "errors" - "time" -) - -type Record struct { - Id uint64 - - Owner string - BadgeCount uint32 - - LastUpdatedAt time.Time - CreatedAt time.Time -} - -func (r *Record) Validate() error { - if len(r.Owner) == 0 { - return errors.New("owner is required") - } - - return nil -} - -func (r *Record) Clone() Record { - return Record{ - Id: r.Id, - - Owner: r.Owner, - BadgeCount: r.BadgeCount, - - LastUpdatedAt: r.LastUpdatedAt, - CreatedAt: r.CreatedAt, - } -} - -func (r *Record) CopyTo(dst *Record) { - dst.Id = r.Id - - dst.Owner = r.Owner - dst.BadgeCount = r.BadgeCount - - dst.LastUpdatedAt = r.LastUpdatedAt - dst.CreatedAt = r.CreatedAt -} diff --git a/pkg/code/data/badgecount/memory/store.go b/pkg/code/data/badgecount/memory/store.go deleted file mode 100644 index 000931bf..00000000 --- a/pkg/code/data/badgecount/memory/store.go +++ /dev/null @@ -1,91 +0,0 @@ -package memory - -import ( - "context" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/code/data/badgecount" -) - -type store struct { - mu sync.Mutex - records []*badgecount.Record - last uint64 -} - -// New returns a new in memory badgecount.Store -func New() badgecount.Store { - return &store{} -} - -// Add implements badgecount.Store.Add -func (s *store) Add(_ context.Context, owner string, amount uint32) error { - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - item := s.findByOwner(owner) - if item != nil { - item.BadgeCount += amount - item.LastUpdatedAt = time.Now() - } else { - s.records = append(s.records, &badgecount.Record{ - Id: s.last, - - Owner: owner, - BadgeCount: amount, - - LastUpdatedAt: time.Now(), - CreatedAt: time.Now(), - }) - } - - return nil -} - -// Reset implements badgecount.Store.Reset -func (s *store) Reset(_ context.Context, owner string) error { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findByOwner(owner) - if item != nil { - s.last++ - item.BadgeCount = 0 - item.LastUpdatedAt = time.Now() - } - - return nil -} - -// Get implements badgecount.Store.Get -func (s *store) Get(_ context.Context, owner string) (*badgecount.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findByOwner(owner) - if item == nil { - return nil, badgecount.ErrBadgeCountNotFound - } - - cloned := item.Clone() - return &cloned, nil -} - -func (s *store) findByOwner(owner string) *badgecount.Record { - for _, item := range s.records { - if item.Owner == owner { - return item - } - } - return nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.records = nil - s.last = 0 -} diff --git a/pkg/code/data/badgecount/memory/store_test.go b/pkg/code/data/badgecount/memory/store_test.go deleted file mode 100644 index 2d53de77..00000000 --- a/pkg/code/data/badgecount/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/badgecount/tests" -) - -func TestBadgeCountMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/badgecount/postgres/model.go b/pkg/code/data/badgecount/postgres/model.go deleted file mode 100644 index dda89f5f..00000000 --- a/pkg/code/data/badgecount/postgres/model.go +++ /dev/null @@ -1,107 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - "github.com/code-payments/code-server/pkg/code/data/badgecount" -) - -const ( - tableName = "codewallet__core_badgecount" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - - Owner string `db:"owner"` - BadgeCount uint32 `db:"badge_count"` - - LastUpdatedAt time.Time `db:"last_updated_at"` - CreatedAt time.Time `db:"created_at"` -} - -func fromModel(obj *model) *badgecount.Record { - return &badgecount.Record{ - Id: uint64(obj.Id.Int64), - - Owner: obj.Owner, - BadgeCount: obj.BadgeCount, - - LastUpdatedAt: obj.LastUpdatedAt, - CreatedAt: obj.CreatedAt, - } -} - -func dbAdd(ctx context.Context, db *sqlx.DB, owner string, amount uint32) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - m := &model{ - Owner: owner, - BadgeCount: amount, - - LastUpdatedAt: time.Now(), - CreatedAt: time.Now(), - } - - query := `INSERT INTO ` + tableName + ` - (owner, badge_count, last_updated_at, created_at) - VALUES ($1, $2, $3, $4) - - ON CONFLICT (owner) - DO UPDATE - SET badge_count = ` + tableName + `.badge_count + $2, last_updated_at = $3 - WHERE ` + tableName + `.owner = $1 - - RETURNING - id, owner, badge_count, last_updated_at, created_at` - - _, err := tx.ExecContext( - ctx, - query, - m.Owner, - m.BadgeCount, - m.LastUpdatedAt, - m.CreatedAt, - ) - return err - }) -} - -func dbReset(ctx context.Context, db *sqlx.DB, owner string) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `UPDATE ` + tableName + ` - SET badge_count = 0, last_updated_at = $2 - WHERE owner = $1 - ` - - _, err := tx.ExecContext( - ctx, - query, - owner, - time.Now(), - ) - return err - }) -} - -func dbGet(ctx context.Context, db *sqlx.DB, owner string) (*model, error) { - res := &model{} - - query := `SELECT id, owner, badge_count, last_updated_at, created_at FROM ` + tableName + ` - WHERE owner = $1 - ` - - err := db.QueryRowxContext( - ctx, - query, - owner, - ).StructScan(res) - if err != nil { - return nil, pgutil.CheckNoRows(err, badgecount.ErrBadgeCountNotFound) - } - return res, nil -} diff --git a/pkg/code/data/badgecount/postgres/store.go b/pkg/code/data/badgecount/postgres/store.go deleted file mode 100644 index 5ded9ca9..00000000 --- a/pkg/code/data/badgecount/postgres/store.go +++ /dev/null @@ -1,40 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/badgecount" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres badgecount.Store -func New(db *sql.DB) badgecount.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Add implements badgecount.Store.Add -func (s *store) Add(ctx context.Context, owner string, amount uint32) error { - return dbAdd(ctx, s.db, owner, amount) -} - -// Reset implements badgecount.Store.Reset -func (s *store) Reset(ctx context.Context, owner string) error { - return dbReset(ctx, s.db, owner) -} - -// Get implements badgecount.Store.Get -func (s *store) Get(ctx context.Context, owner string) (*badgecount.Record, error) { - model, err := dbGet(ctx, s.db, owner) - if err != nil { - return nil, err - } - return fromModel(model), nil -} diff --git a/pkg/code/data/badgecount/postgres/store_test.go b/pkg/code/data/badgecount/postgres/store_test.go deleted file mode 100644 index 96eaf2bb..00000000 --- a/pkg/code/data/badgecount/postgres/store_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/badgecount" - "github.com/code-payments/code-server/pkg/code/data/badgecount/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -var ( - testStore badgecount.Store - teardown func() -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_badgecount ( - id SERIAL NOT NULL PRIMARY KEY, - - owner TEXT NOT NULL, - badge_count INTEGER NOT NULL, - - last_updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_chat__uniq__owner UNIQUE (owner) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_badgecount; - ` -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestBadgeCountPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/badgecount/store.go b/pkg/code/data/badgecount/store.go deleted file mode 100644 index a97e4f36..00000000 --- a/pkg/code/data/badgecount/store.go +++ /dev/null @@ -1,21 +0,0 @@ -package badgecount - -import ( - "context" - "errors" -) - -var ( - ErrBadgeCountNotFound = errors.New("badge count not found") -) - -type Store interface { - // Add adds to the owner account's badge count - Add(ctx context.Context, owner string, amount uint32) error - - // Reset resets the badge count for an owner account to zero - Reset(ctx context.Context, owner string) error - - // Get gets a badge count record for an owner - Get(ctx context.Context, owner string) (*Record, error) -} diff --git a/pkg/code/data/badgecount/tests/tests.go b/pkg/code/data/badgecount/tests/tests.go deleted file mode 100644 index 36d5828d..00000000 --- a/pkg/code/data/badgecount/tests/tests.go +++ /dev/null @@ -1,77 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/badgecount" -) - -func RunTests(t *testing.T, s badgecount.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s badgecount.Store){ - testHappyPath, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s badgecount.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - start := time.Now() - time.Sleep(time.Millisecond) - - _, err := s.Get(ctx, "owner1") - assert.Equal(t, badgecount.ErrBadgeCountNotFound, err) - - require.NoError(t, s.Add(ctx, "owner1", 1)) - require.NoError(t, s.Add(ctx, "owner2", 5)) - - actual, err := s.Get(ctx, "owner1") - require.NoError(t, err) - require.NoError(t, actual.Validate()) - assert.True(t, actual.Id > 0) - assert.Equal(t, "owner1", actual.Owner) - assert.EqualValues(t, 1, actual.BadgeCount) - assert.True(t, actual.LastUpdatedAt.After(start)) - assert.True(t, actual.CreatedAt.After(start)) - - start = time.Now() - time.Sleep(time.Millisecond) - - require.NoError(t, s.Add(ctx, "owner1", 2)) - - actual, err = s.Get(ctx, "owner1") - require.NoError(t, err) - require.NoError(t, actual.Validate()) - assert.Equal(t, "owner1", actual.Owner) - assert.EqualValues(t, 3, actual.BadgeCount) - assert.True(t, actual.LastUpdatedAt.After(start)) - assert.True(t, actual.CreatedAt.Before(start)) - - start = time.Now() - time.Sleep(time.Millisecond) - - require.NoError(t, s.Reset(ctx, "owner1")) - - actual, err = s.Get(ctx, "owner1") - require.NoError(t, err) - require.NoError(t, actual.Validate()) - assert.Equal(t, "owner1", actual.Owner) - assert.EqualValues(t, 0, actual.BadgeCount) - assert.True(t, actual.LastUpdatedAt.After(start)) - assert.True(t, actual.CreatedAt.Before(start)) - - actual, err = s.Get(ctx, "owner2") - require.NoError(t, err) - require.NoError(t, actual.Validate()) - assert.Equal(t, "owner2", actual.Owner) - assert.EqualValues(t, 5, actual.BadgeCount) - }) -} diff --git a/pkg/code/data/blockchain.go b/pkg/code/data/blockchain.go index deabb706..e5232359 100644 --- a/pkg/code/data/blockchain.go +++ b/pkg/code/data/blockchain.go @@ -6,8 +6,8 @@ import ( "github.com/mr-tron/base58" + "github.com/code-payments/code-server/pkg/code/config" "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/token" @@ -46,7 +46,7 @@ type BlockchainProvider struct { func NewBlockchainProvider(solanaEndpoint string) (BlockchainData, error) { sc := solana.New(solanaEndpoint) - tc := token.NewClient(sc, kin.TokenMint) + tc := token.NewClient(sc, config.CoreMintPublicKeyBytes) return &BlockchainProvider{ sc: sc, @@ -141,7 +141,7 @@ func (dp *BlockchainProvider) GetBlockchainTokenAccountsByOwner(ctx context.Cont return nil, err } - res, err := dp.sc.GetTokenAccountsByOwner(accountId, kin.TokenMint) + res, err := dp.sc.GetTokenAccountsByOwner(accountId, config.CoreMintPublicKeyBytes) if err != nil { tracer.OnError(err) diff --git a/pkg/code/data/chat/memory/store.go b/pkg/code/data/chat/memory/store.go deleted file mode 100644 index 1b869007..00000000 --- a/pkg/code/data/chat/memory/store.go +++ /dev/null @@ -1,440 +0,0 @@ -package memory - -import ( - "bytes" - "context" - "sort" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/chat" -) - -type ChatsById []*chat.Chat - -func (a ChatsById) Len() int { return len(a) } -func (a ChatsById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ChatsById) Less(i, j int) bool { return a[i].Id < a[j].Id } - -type MessagesByTimestampAndId []*chat.Message - -func (a MessagesByTimestampAndId) Len() int { return len(a) } -func (a MessagesByTimestampAndId) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a MessagesByTimestampAndId) Less(i, j int) bool { - if a[i].Timestamp.Before(a[j].Timestamp) { - return true - } - - if a[i].Timestamp.Equal(a[j].Timestamp) && a[i].Id < a[j].Id { - return true - } - - return false -} - -type store struct { - mu sync.Mutex - chatRecords []*chat.Chat - messageRecords []*chat.Message - last uint64 -} - -// New returns a new in memory chat.Store -func New() chat.Store { - return &store{} -} - -// PutChat implements chat.Store.PutChat -func (s *store) PutChat(ctx context.Context, data *chat.Chat) error { - if err := data.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - if item := s.findChat(data); item != nil { - return chat.ErrChatAlreadyExists - } - - data.Id = s.last - if data.CreatedAt.IsZero() { - data.CreatedAt = time.Now() - } - - cloned := data.Clone() - s.chatRecords = append(s.chatRecords, &cloned) - - return nil -} - -// GetChatById implements chat.Store.GetChatById -func (s *store) GetChatById(ctx context.Context, id chat.ChatId) (*chat.Chat, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findChatById(id) - if item == nil { - return nil, chat.ErrChatNotFound - } - - cloned := item.Clone() - return &cloned, nil -} - -// GetAllChatsForUser implements chat.Store.GetAllChatsForUser -func (s *store) GetAllChatsForUser(ctx context.Context, user string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.Chat, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findChatsByUser(user) - items = s.filterPagedChats(items, cursor, direction, limit) - if len(items) == 0 { - return nil, chat.ErrChatNotFound - } - - return cloneChats(items), nil -} - -// PutMessage implements chat.Store.PutMessage -func (s *store) PutMessage(ctx context.Context, data *chat.Message) error { - if err := data.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - if item := s.findMessage(data); item != nil { - return chat.ErrMessageAlreadyExists - } - - data.Id = s.last - - cloned := data.Clone() - s.messageRecords = append(s.messageRecords, &cloned) - - return nil -} - -// DeleteMessage implements chat.Store.DeleteMessage -func (s *store) DeleteMessage(ctx context.Context, chatId chat.ChatId, messageId string) error { - s.mu.Lock() - defer s.mu.Unlock() - - for i, item := range s.messageRecords { - if bytes.Equal(item.ChatId[:], chatId[:]) && item.MessageId == messageId { - s.messageRecords = append(s.messageRecords[:i], s.messageRecords[i+1:]...) - return nil - } - } - - return nil -} - -// GetMessageById implements chat.Store.GetMessageById -func (s *store) GetMessageById(ctx context.Context, chatId chat.ChatId, messageId string) (*chat.Message, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findMessageById(chatId, messageId) - if item == nil { - return nil, chat.ErrMessageNotFound - } - - cloned := item.Clone() - return &cloned, nil -} - -// GetAllMessagesByChat implements chat.Store.GetAllMessagesByChat -func (s *store) GetAllMessagesByChat(ctx context.Context, chatId chat.ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.Message, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findMessagesByChatId(chatId) - items, err := s.filterPagedMessagesByChat(items, cursor, direction, limit) - if err != nil { - return nil, err - } - if len(items) == 0 { - return nil, chat.ErrMessageNotFound - } - - return cloneMessages(items), nil -} - -// AdvancePointer implements chat.Store.AdvancePointer -func (s *store) AdvancePointer(_ context.Context, chatId chat.ChatId, pointer string) error { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findChatById(chatId) - if item == nil { - return chat.ErrChatNotFound - } - - item.ReadPointer = &pointer - - return nil -} - -// GetUnreadCount implements chat.Store.GetUnreadCount -func (s *store) GetUnreadCount(ctx context.Context, chatId chat.ChatId) (uint32, error) { - s.mu.Lock() - defer s.mu.Unlock() - - chatItem := s.findChatById(chatId) - if chatItem == nil { - return 0, nil - } - - var after time.Time - if chatItem.ReadPointer != nil { - messageItem := s.findMessageById(chatId, *chatItem.ReadPointer) - if messageItem != nil { - after = messageItem.Timestamp - } - } - - messageItems := s.findMessagesByChatId(chatId) - messageItems = s.filterMessagesAfter(messageItems, after) - messageItems = s.filterNotifiedMessages(messageItems) - return uint32(len(messageItems)), nil -} - -// SetMuteState implements chat.Store.SetMuteState -func (s *store) SetMuteState(ctx context.Context, chatId chat.ChatId, isMuted bool) error { - s.mu.Lock() - defer s.mu.Unlock() - - chatItem := s.findChatById(chatId) - if chatItem == nil { - return chat.ErrChatNotFound - } - - chatItem.IsMuted = isMuted - - return nil -} - -// SetSubscriptionState implements chat.Store.SetSubscriptionState -func (s *store) SetSubscriptionState(ctx context.Context, chatId chat.ChatId, isSubscribed bool) error { - s.mu.Lock() - defer s.mu.Unlock() - - chatItem := s.findChatById(chatId) - if chatItem == nil { - return chat.ErrChatNotFound - } - - chatItem.IsUnsubscribed = !isSubscribed - - return nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.chatRecords = nil - s.messageRecords = nil - s.last = 0 -} - -func (s *store) findChat(data *chat.Chat) *chat.Chat { - for _, item := range s.chatRecords { - if item.Id == data.Id { - return item - } - - if bytes.Equal(data.ChatId[:], item.ChatId[:]) { - return item - } - } - return nil -} - -func (s *store) findChatById(id chat.ChatId) *chat.Chat { - for _, item := range s.chatRecords { - if bytes.Equal(id[:], item.ChatId[:]) { - return item - } - } - return nil -} - -func (s *store) findChatsByUser(user string) []*chat.Chat { - var res []*chat.Chat - for _, item := range s.chatRecords { - if item.CodeUser == user { - res = append(res, item) - } - } - return res -} - -func (s *store) findMessage(data *chat.Message) *chat.Message { - for _, item := range s.messageRecords { - if item.Id == data.Id { - return item - } - - if bytes.Equal(item.ChatId[:], data.ChatId[:]) && item.MessageId == data.MessageId { - return item - } - } - return nil -} - -func (s *store) findMessageById(chatId chat.ChatId, messageId string) *chat.Message { - for _, item := range s.messageRecords { - if bytes.Equal(item.ChatId[:], chatId[:]) && item.MessageId == messageId { - return item - } - } - return nil -} - -func (s *store) findMessagesByChatId(chatId chat.ChatId) []*chat.Message { - var res []*chat.Message - for _, item := range s.messageRecords { - if bytes.Equal(chatId[:], item.ChatId[:]) { - res = append(res, item) - } - } - return res -} - -func (s *store) filterMessagesAfter(items []*chat.Message, ts time.Time) []*chat.Message { - var res []*chat.Message - for _, item := range items { - if item.Timestamp.After(ts) { - res = append(res, item) - } - } - return res -} - -func (s *store) filterNotifiedMessages(items []*chat.Message) []*chat.Message { - var res []*chat.Message - for _, item := range items { - if !item.IsSilent { - res = append(res, item) - } - } - return res -} - -func (s *store) filterPagedChats(items []*chat.Chat, cursor query.Cursor, direction query.Ordering, limit uint64) []*chat.Chat { - var start uint64 - - start = 0 - if direction == query.Descending { - start = s.last + 1 - } - if len(cursor) > 0 { - start = cursor.ToUint64() - } - - var res []*chat.Chat - for _, item := range items { - if item.Id > start && direction == query.Ascending { - res = append(res, item) - } - if item.Id < start && direction == query.Descending { - res = append(res, item) - } - } - - if direction == query.Ascending { - sort.Sort(ChatsById(res)) - } else { - sort.Sort(sort.Reverse(ChatsById(res))) - } - - if len(res) >= int(limit) { - return res[:limit] - } - - return res -} - -func (s *store) filterPagedMessagesByChat(items []*chat.Message, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.Message, error) { - if len(items) == 0 { - return nil, nil - } - - var recordCursor *chat.Message - if len(cursor) > 0 { - recordCursor = s.findMessageById(items[0].ChatId, cursor.ToBase58()) - if recordCursor == nil { - return nil, chat.ErrInvalidMessageCursor - } - } - - var res []*chat.Message - if recordCursor == nil { - res = items - } else { - for _, item := range items { - if item.Timestamp.Equal(recordCursor.Timestamp) { - if item.Id > recordCursor.Id && direction == query.Ascending { - res = append(res, item) - } - - if item.Id < recordCursor.Id && direction == query.Descending { - res = append(res, item) - } - } - - if item.Timestamp.After(recordCursor.Timestamp) && direction == query.Ascending { - res = append(res, item) - } - - if item.Timestamp.Before(recordCursor.Timestamp) && direction == query.Descending { - res = append(res, item) - } - } - } - - if direction == query.Ascending { - sort.Sort(MessagesByTimestampAndId(res)) - } else { - sort.Sort(sort.Reverse(MessagesByTimestampAndId(res))) - } - - if len(res) >= int(limit) { - return res[:limit], nil - } - - return res, nil -} - -func (s *store) sumContentLengths(items []*chat.Message) uint32 { - var res uint32 - for _, item := range items { - res += uint32(item.ContentLength) - } - return res -} - -func cloneChats(items []*chat.Chat) []*chat.Chat { - res := make([]*chat.Chat, len(items)) - for i, item := range items { - cloned := item.Clone() - res[i] = &cloned - } - return res -} - -func cloneMessages(items []*chat.Message) []*chat.Message { - res := make([]*chat.Message, len(items)) - for i, item := range items { - cloned := item.Clone() - res[i] = &cloned - } - return res -} diff --git a/pkg/code/data/chat/memory/store_test.go b/pkg/code/data/chat/memory/store_test.go deleted file mode 100644 index 5d2c18a5..00000000 --- a/pkg/code/data/chat/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/chat/tests" -) - -func TestChatMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/chat/model.go b/pkg/code/data/chat/model.go deleted file mode 100644 index d8fe7432..00000000 --- a/pkg/code/data/chat/model.go +++ /dev/null @@ -1,203 +0,0 @@ -package chat - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "strings" - "time" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - - "github.com/code-payments/code-server/pkg/pointer" -) - -type ChatType uint8 - -const ( - ChatTypeUnknown ChatType = iota - ChatTypeInternal // todo: better name, or split into the various buckets (eg. Code Team vs Cash Transactions) - ChatTypeExternalApp -) - -type ChatId [32]byte - -type Chat struct { - Id uint64 - - ChatId ChatId - ChatType ChatType - ChatTitle string // The message sender - IsVerified bool - - CodeUser string // Always a receiver of messages - - ReadPointer *string - IsMuted bool - IsUnsubscribed bool - - CreatedAt time.Time -} - -type Message struct { - Id uint64 - - ChatId ChatId - - MessageId string - Data []byte - - IsSilent bool - ContentLength uint8 - - Timestamp time.Time -} - -func GetChatId(sender, receiver string, isVerified bool) ChatId { - combined := []byte(fmt.Sprintf("%s:%s:%v", sender, receiver, isVerified)) - if strings.Compare(sender, receiver) > 0 { - combined = []byte(fmt.Sprintf("%s:%s:%v", receiver, sender, isVerified)) - } - return sha256.Sum256(combined) -} - -func (c ChatId) ToProto() *chatpb.ChatId { - return &chatpb.ChatId{ - Value: c[:], - } -} - -func ChatIdFromProto(proto *chatpb.ChatId) ChatId { - var chatId ChatId - copy(chatId[:], proto.Value) - return chatId -} - -func (c ChatId) String() string { - return hex.EncodeToString(c[:]) -} - -func (r *Chat) Validate() error { - expectedChatId := GetChatId(r.CodeUser, r.ChatTitle, r.IsVerified) - if !bytes.Equal(r.ChatId[:], expectedChatId[:]) { - return errors.New("chat id is invalid") - } - - if r.ChatType == ChatTypeUnknown { - return errors.New("chat type is required") - } - - if len(r.ChatTitle) == 0 { - return errors.New("chat title is required") - } - - if len(r.CodeUser) == 0 { - return errors.New("code user is required") - } - - if r.ReadPointer != nil && len(*r.ReadPointer) == 0 { - return errors.New("read pointer is required when set") - } - - return nil -} - -func (r *Chat) Clone() Chat { - var chatIdCopy ChatId - copy(chatIdCopy[:], r.ChatId[:]) - - return Chat{ - Id: r.Id, - - ChatId: chatIdCopy, - ChatType: r.ChatType, - ChatTitle: r.ChatTitle, - IsVerified: r.IsVerified, - - CodeUser: r.CodeUser, - - ReadPointer: pointer.StringCopy(r.ReadPointer), - - IsMuted: r.IsMuted, - IsUnsubscribed: r.IsUnsubscribed, - - CreatedAt: r.CreatedAt, - } -} - -func (r *Chat) CopyTo(dst *Chat) { - dst.Id = r.Id - - copy(dst.ChatId[:], r.ChatId[:]) - dst.ChatType = r.ChatType - dst.ChatTitle = r.ChatTitle - dst.IsVerified = r.IsVerified - - dst.CodeUser = r.CodeUser - - dst.ReadPointer = pointer.StringCopy(r.ReadPointer) - - dst.IsMuted = r.IsMuted - dst.IsUnsubscribed = r.IsUnsubscribed - - dst.CreatedAt = r.CreatedAt -} - -func (r *Message) Validate() error { - if len(r.Data) == 0 { - return errors.New("data is required") - } - - if len(r.MessageId) == 0 { - return errors.New("message id is required") - } - - if r.ContentLength <= 0 { - return errors.New("content length must be positive") - } - - if r.Timestamp.IsZero() { - return errors.New("timestamp is required") - } - - return nil -} - -func (r *Message) Clone() Message { - var chatIdCopy ChatId - copy(chatIdCopy[:], r.ChatId[:]) - - dataCopy := make([]byte, len(r.Data)) - copy(dataCopy, r.Data) - - return Message{ - Id: r.Id, - - ChatId: chatIdCopy, - - MessageId: r.MessageId, - Data: dataCopy, - - IsSilent: r.IsSilent, - ContentLength: r.ContentLength, - - Timestamp: r.Timestamp, - } -} - -func (r *Message) CopyTo(dst *Message) { - dst.Id = r.Id - - dst.ChatId = r.ChatId - - dst.MessageId = r.MessageId - dst.Data = make([]byte, len(r.Data)) - copy(dst.Data, r.Data) - - dst.IsSilent = r.IsSilent - dst.ContentLength = r.ContentLength - - dst.Timestamp = r.Timestamp -} diff --git a/pkg/code/data/chat/model_test.go b/pkg/code/data/chat/model_test.go deleted file mode 100644 index 7774d286..00000000 --- a/pkg/code/data/chat/model_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package chat - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestChatId(t *testing.T) { - chatId1 := GetChatId("sender1", "receiver1", true) - chatId2 := GetChatId("receiver1", "sender1", true) - chatId3 := GetChatId("sender2", "receiver2", false) - chatId4 := GetChatId("receiver2", "sender2", false) - chatId5 := GetChatId("sender1", "receiver2", false) - chatId6 := GetChatId("sender2", "receiver1", false) - assert.Equal(t, chatId1, chatId2) - assert.Equal(t, chatId3, chatId4) - assert.NotEqual(t, chatId1, chatId3) - assert.NotEqual(t, chatId2, chatId4) - assert.NotEqual(t, chatId1, chatId5) - assert.NotEqual(t, chatId3, chatId6) -} diff --git a/pkg/code/data/chat/postgres/model.go b/pkg/code/data/chat/postgres/model.go deleted file mode 100644 index 07158095..00000000 --- a/pkg/code/data/chat/postgres/model.go +++ /dev/null @@ -1,413 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "fmt" - "time" - - "github.com/jmoiron/sqlx" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - q "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/chat" -) - -const ( - chatTableName = "codewallet__core_chat" - messageTableName = "codewallet__core_chatmessage" -) - -type chatModel struct { - Id sql.NullInt64 `db:"id"` - - ChatId []byte `db:"chat_id"` - ChatType uint8 `db:"chat_type"` - IsVerified bool `db:"is_verified"` - - Member1 string `db:"member1"` - Member2 string `db:"member2"` // Keeping this open in case we want to have bidirectional chats across users - - ReadPointer sql.NullString `db:"read_pointer"` - IsMuted bool `db:"is_muted"` - IsUnsubscribed bool `db:"is_unsubscribed"` - - CreatedAt time.Time `db:"created_at"` -} - -type messageModel struct { - Id sql.NullInt64 `db:"id"` - - ChatId []byte `db:"chat_id"` - - MessageId string `db:"message_id"` - Data []byte `db:"data"` - - IsSilent bool `db:"is_silent"` - ContentLength uint8 `db:"content_length"` - - Timestamp time.Time `db:"timestamp"` -} - -func toChatModel(obj *chat.Chat) (*chatModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &chatModel{ - ChatId: obj.ChatId[:], - ChatType: uint8(obj.ChatType), - IsVerified: obj.IsVerified, - - Member1: obj.CodeUser, - Member2: obj.ChatTitle, - - ReadPointer: sql.NullString{ - Valid: obj.ReadPointer != nil, - String: *pointer.StringOrDefault(obj.ReadPointer, ""), - }, - IsMuted: obj.IsMuted, - IsUnsubscribed: obj.IsUnsubscribed, - - CreatedAt: obj.CreatedAt, - }, nil -} - -func fromChatModel(obj *chatModel) *chat.Chat { - var chatId chat.ChatId - copy(chatId[:], obj.ChatId) - - return &chat.Chat{ - Id: uint64(obj.Id.Int64), - - ChatId: chatId, - ChatType: chat.ChatType(obj.ChatType), - IsVerified: obj.IsVerified, - - CodeUser: obj.Member1, - ChatTitle: obj.Member2, - - ReadPointer: pointer.StringIfValid(obj.ReadPointer.Valid, obj.ReadPointer.String), - IsMuted: obj.IsMuted, - IsUnsubscribed: obj.IsUnsubscribed, - - CreatedAt: obj.CreatedAt, - } -} - -func toMessageModel(obj *chat.Message) (*messageModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &messageModel{ - ChatId: obj.ChatId[:], - - MessageId: obj.MessageId, - Data: obj.Data, - - IsSilent: obj.IsSilent, - ContentLength: obj.ContentLength, - - Timestamp: obj.Timestamp, - }, nil -} - -func fromMessageModel(obj *messageModel) *chat.Message { - var chatId chat.ChatId - copy(chatId[:], obj.ChatId) - - return &chat.Message{ - Id: uint64(obj.Id.Int64), - - ChatId: chatId, - - MessageId: obj.MessageId, - Data: obj.Data, - - IsSilent: obj.IsSilent, - ContentLength: obj.ContentLength, - - Timestamp: obj.Timestamp, - } -} - -func (m *chatModel) dbPut(ctx context.Context, db *sqlx.DB) error { - err := pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `INSERT INTO ` + chatTableName + ` - (chat_id, chat_type, is_verified, member1, member2, read_pointer, is_muted, is_unsubscribed, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - RETURNING id, chat_id, chat_type, is_verified, member1, member2, created_at - ` - - if m.CreatedAt.IsZero() { - m.CreatedAt = time.Now() - } - - return tx.QueryRowxContext( - ctx, - query, - m.ChatId, - m.ChatType, - m.IsVerified, - m.Member1, - m.Member2, - m.ReadPointer, - m.IsMuted, - m.IsUnsubscribed, - m.CreatedAt, - ).StructScan(m) - }) - return pgutil.CheckUniqueViolation(err, chat.ErrChatAlreadyExists) -} - -func (m *messageModel) dbPut(ctx context.Context, db *sqlx.DB) error { - err := pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `INSERT INTO ` + messageTableName + ` - (chat_id, message_id, data, is_silent, content_length, timestamp) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id, chat_id, message_id, data, timestamp - ` - - return tx.QueryRowxContext( - ctx, - query, - m.ChatId, - m.MessageId, - m.Data, - m.IsSilent, - m.ContentLength, - m.Timestamp, - ).StructScan(m) - }) - return pgutil.CheckUniqueViolation(err, chat.ErrMessageAlreadyExists) -} - -func dbGetChatById(ctx context.Context, db *sqlx.DB, id chat.ChatId) (*chatModel, error) { - res := &chatModel{} - - query := `SELECT id, chat_id, chat_type, is_verified, member1, member2, read_pointer, is_muted, is_unsubscribed, created_at FROM ` + chatTableName + ` - WHERE chat_id = $1 - ` - - err := db.QueryRowxContext( - ctx, - query, - id[:], - ).StructScan(res) - if err != nil { - return nil, pgutil.CheckNoRows(err, chat.ErrChatNotFound) - } - return res, nil -} - -func dbDeleteMessage(ctx context.Context, db *sqlx.DB, chatId chat.ChatId, messageId string) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `DELETE FROM ` + messageTableName + ` - WHERE chat_id = $1 AND message_id = $2 - ` - - _, err := db.ExecContext( - ctx, - query, - chatId[:], - messageId, - ) - return err - }) -} - -func dbGetMessageById(ctx context.Context, db *sqlx.DB, chatId chat.ChatId, messageId string) (*messageModel, error) { - res := &messageModel{} - - query := `SELECT id, chat_id, message_id, data, is_silent, content_length, timestamp FROM ` + messageTableName + ` - WHERE chat_id = $1 AND message_id = $2 - ` - - err := db.QueryRowxContext( - ctx, - query, - chatId[:], - messageId, - ).StructScan(res) - if err != nil { - return nil, pgutil.CheckNoRows(err, chat.ErrMessageNotFound) - } - return res, nil -} - -func dbGetAllChatsForUser(ctx context.Context, db *sqlx.DB, user string, cursor q.Cursor, direction q.Ordering, limit uint64) ([]*chatModel, error) { - res := []*chatModel{} - - query := `SELECT id, chat_id, chat_type, is_verified, member1, member2, read_pointer, is_muted, is_unsubscribed, created_at FROM ` + chatTableName + ` - WHERE member1 = $1` - - opts := []interface{}{user} - query, opts = q.PaginateQuery(query, opts, cursor, limit, direction) - - err := db.SelectContext( - ctx, - &res, - query, - opts..., - ) - if err != nil { - return nil, pgutil.CheckNoRows(err, chat.ErrChatNotFound) - } else if len(res) == 0 { - return nil, chat.ErrChatNotFound - } - return res, nil -} - -func dbGetAllMessagesByChat(ctx context.Context, db *sqlx.DB, chatId chat.ChatId, cursor q.Cursor, direction q.Ordering, limit uint64) ([]*messageModel, error) { - res := []*messageModel{} - - query := `SELECT id, chat_id, message_id, data, is_silent, content_length, timestamp FROM ` + messageTableName + ` - WHERE chat_id = $1` - - opts := []interface{}{chatId[:]} - - if len(cursor) > 0 { - // todo: optimize to a single query - messageModel, err := dbGetMessageById(ctx, db, chatId, cursor.ToBase58()) - if err == chat.ErrMessageNotFound { - return nil, chat.ErrInvalidMessageCursor - } - - if direction == q.Ascending { - query += fmt.Sprintf(" AND (timestamp > $%d OR (timestamp = $%d AND id > $%d))", len(opts)+1, len(opts)+1, len(opts)+2) - } else { - query += fmt.Sprintf(" AND (timestamp < $%d OR (timestamp = $%d AND id < $%d))", len(opts)+1, len(opts)+1, len(opts)+2) - } - - opts = append(opts, messageModel.Timestamp, messageModel.Id) - } - - // Optimize for timestamp, but in the event messages fall in the same time, - // fall back to DB insertion order. - if direction == q.Ascending { - query += " ORDER BY timestamp ASC, id ASC" - } else { - query += " ORDER BY timestamp DESC, id DESC" - } - - if limit > 0 { - query += fmt.Sprintf(" LIMIT $%d", len(opts)+1) - opts = append(opts, limit) - } - - err := db.SelectContext( - ctx, - &res, - query, - opts..., - ) - if err != nil { - return nil, pgutil.CheckNoRows(err, chat.ErrMessageNotFound) - } else if len(res) == 0 { - return nil, chat.ErrMessageNotFound - } - return res, nil -} - -func dbAdvancePointer(ctx context.Context, db *sqlx.DB, chatId chat.ChatId, pointer string) error { - query := `UPDATE ` + chatTableName + ` - SET read_pointer = $2 - WHERE chat_id = $1 - ` - - res, err := db.ExecContext( - ctx, - query, - chatId[:], - pointer, - ) - if err != nil { - return err - } - - rowsAffected, err := res.RowsAffected() - if err != nil { - return err - } else if rowsAffected == 0 { - return chat.ErrChatNotFound - } - - return nil -} - -func dbGetUnreadCount(ctx context.Context, db *sqlx.DB, chatId chat.ChatId) (uint32, error) { - res := &struct { - UnreadCount sql.NullInt64 `db:"unread_count"` - }{} - - query := `SELECT COUNT(*) AS unread_count FROM ` + messageTableName + ` WHERE - chat_id = $1 AND - timestamp > COALESCE((SELECT timestamp FROM ` + messageTableName + ` WHERE chat_id = $1 AND message_id = (SELECT read_pointer FROM ` + chatTableName + ` WHERE chat_id = $1)), $2) - AND NOT is_silent - ` - - err := db.QueryRowxContext( - ctx, - query, - chatId[:], - time.Time{}, - ).StructScan(res) - if err != nil { - return 0, err - } - return uint32(res.UnreadCount.Int64), nil -} - -func dbSetMuteState(ctx context.Context, db *sqlx.DB, chatId chat.ChatId, isMuted bool) error { - query := `UPDATE ` + chatTableName + ` - SET is_muted = $2 - WHERE chat_id = $1 - ` - - res, err := db.ExecContext( - ctx, - query, - chatId[:], - isMuted, - ) - if err != nil { - return err - } - - rowsAffected, err := res.RowsAffected() - if err != nil { - return err - } else if rowsAffected == 0 { - return chat.ErrChatNotFound - } - - return nil -} - -func dbSetSubscriptionState(ctx context.Context, db *sqlx.DB, chatId chat.ChatId, isSubscribed bool) error { - query := `UPDATE ` + chatTableName + ` - SET is_unsubscribed = $2 - WHERE chat_id = $1 - ` - - res, err := db.ExecContext( - ctx, - query, - chatId[:], - !isSubscribed, - ) - if err != nil { - return err - } - - rowsAffected, err := res.RowsAffected() - if err != nil { - return err - } else if rowsAffected == 0 { - return chat.ErrChatNotFound - } - - return nil -} diff --git a/pkg/code/data/chat/postgres/store.go b/pkg/code/data/chat/postgres/store.go deleted file mode 100644 index 943a1935..00000000 --- a/pkg/code/data/chat/postgres/store.go +++ /dev/null @@ -1,129 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/chat" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres-backed chat.Store -func New(db *sql.DB) chat.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// PutChat implements chat.Store.PutChat -func (s *store) PutChat(ctx context.Context, record *chat.Chat) error { - model, err := toChatModel(record) - if err != nil { - return err - } - - err = model.dbPut(ctx, s.db) - if err != nil { - return err - } - - fromChatModel(model).CopyTo(record) - - return nil -} - -// GetChatById implements chat.Store.GetChatById -func (s *store) GetChatById(ctx context.Context, id chat.ChatId) (*chat.Chat, error) { - model, err := dbGetChatById(ctx, s.db, id) - if err != nil { - return nil, err - } - - return fromChatModel(model), nil -} - -// GetAllChatsForUser implements chat.Store.GetAllChatsForUser -func (s *store) GetAllChatsForUser(ctx context.Context, user string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.Chat, error) { - models, err := dbGetAllChatsForUser(ctx, s.db, user, cursor, direction, limit) - if err != nil { - return nil, err - } - - var res []*chat.Chat - for _, model := range models { - res = append(res, fromChatModel(model)) - } - return res, nil -} - -// PutMessage implements chat.Store.PutMessage -func (s *store) PutMessage(ctx context.Context, record *chat.Message) error { - model, err := toMessageModel(record) - if err != nil { - return err - } - - err = model.dbPut(ctx, s.db) - if err != nil { - return err - } - - fromMessageModel(model).CopyTo(record) - - return nil -} - -// DeleteMessage implements chat.Store.DeleteMessage -func (s *store) DeleteMessage(ctx context.Context, chatId chat.ChatId, messageId string) error { - return dbDeleteMessage(ctx, s.db, chatId, messageId) -} - -// GetMessageById implements chat.Store.GetMessageById -func (s *store) GetMessageById(ctx context.Context, chatId chat.ChatId, messageId string) (*chat.Message, error) { - model, err := dbGetMessageById(ctx, s.db, chatId, messageId) - if err != nil { - return nil, err - } - - return fromMessageModel(model), nil -} - -// GetAllMessagesByChat implements chat.Store.GetAllMessagesByChat -func (s *store) GetAllMessagesByChat(ctx context.Context, chatId chat.ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.Message, error) { - models, err := dbGetAllMessagesByChat(ctx, s.db, chatId, cursor, direction, limit) - if err != nil { - return nil, err - } - - var res []*chat.Message - for _, model := range models { - res = append(res, fromMessageModel(model)) - } - return res, nil -} - -// AdvancePointer implements chat.Store.AdvancePointer -func (s *store) AdvancePointer(ctx context.Context, chatId chat.ChatId, pointer string) error { - return dbAdvancePointer(ctx, s.db, chatId, pointer) -} - -// GetUnreadCount implements chat.Store.GetUnreadCount -func (s *store) GetUnreadCount(ctx context.Context, chatId chat.ChatId) (uint32, error) { - return dbGetUnreadCount(ctx, s.db, chatId) -} - -// SetMuteState implements chat.Store.SetMuteState -func (s *store) SetMuteState(ctx context.Context, chatId chat.ChatId, isMuted bool) error { - return dbSetMuteState(ctx, s.db, chatId, isMuted) -} - -// SetSubscriptionState implements chat.Store.SetSubscriptionState -func (s *store) SetSubscriptionState(ctx context.Context, chatId chat.ChatId, isSubscribed bool) error { - return dbSetSubscriptionState(ctx, s.db, chatId, isSubscribed) -} diff --git a/pkg/code/data/chat/postgres/store_test.go b/pkg/code/data/chat/postgres/store_test.go deleted file mode 100644 index 49143ad7..00000000 --- a/pkg/code/data/chat/postgres/store_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/chat/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -var ( - testStore chat.Store - teardown func() -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_chat ( - id SERIAL NOT NULL PRIMARY KEY, - - chat_id BYTEA NOT NULL, - chat_type INTEGER NOT NULL, - is_verified BOOL NOT NULL, - - member1 TEXT NOT NULL, - member2 TEXT NOT NULL, - - read_pointer TEXT NULL, - - is_muted BOOL NOT NULL, - is_unsubscribed BOOL NOT NULL, - - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_chat__uniq__chat_id UNIQUE (chat_id) - ); - - CREATE TABLE codewallet__core_chatmessage ( - id SERIAL NOT NULL PRIMARY KEY, - - chat_id BYTEA NOT NULL, - - message_id TEXT NOT NULL, - data BYTEA NOT NULL, - - is_silent BOOL NOT NULL, - content_length INTEGER NOT NULL, - - timestamp TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_chatmessage__uniq__chat_id__and__message_id UNIQUE (chat_id, message_id) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_chat; - DROP TABLE codewallet__core_chatmessage; - ` -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestChatPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/chat/store.go b/pkg/code/data/chat/store.go deleted file mode 100644 index 2e79a228..00000000 --- a/pkg/code/data/chat/store.go +++ /dev/null @@ -1,56 +0,0 @@ -package chat - -import ( - "context" - "errors" - - "github.com/code-payments/code-server/pkg/database/query" -) - -var ( - ErrChatAlreadyExists = errors.New("chat record already exists") - ErrChatNotFound = errors.New("chat record not found") - ErrMessageAlreadyExists = errors.New("message record already exists") - ErrMessageNotFound = errors.New("message record not found") - ErrInvalidMessageCursor = errors.New("message cursor is invalid") -) - -type Store interface { - // PutChat creates a new chat metadata - PutChat(ctx context.Context, record *Chat) error - - // GetChatById gets a chat by its chat ID - GetChatById(ctx context.Context, chatId ChatId) (*Chat, error) - - // GetAllChatsForUser gets all chats for a given user - // - // Note: Cursor is the auto-incrementing ID - GetAllChatsForUser(ctx context.Context, user string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*Chat, error) - - // PutMessage creates a new new chat message - PutMessage(ctx context.Context, record *Message) error - - // Delete message deletes a message within a chat. The call is idempotent - // and will not fail if the message doesn't exist. - DeleteMessage(ctx context.Context, chatId ChatId, messageId string) error - - // GetMessageById gets a chat message by its message ID within a chat - GetMessageById(ctx context.Context, chatId ChatId, messageId string) (*Message, error) - - // GetAllMessagesByChat gets all messages for a given chat - // - // Note: Cursor is a message ID - GetAllMessagesByChat(ctx context.Context, chatId ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*Message, error) - - // AdvancePointer advances a chat pointer - AdvancePointer(ctx context.Context, chatId ChatId, pointer string) error - - // GetUnreadCount gets the unread message count for a chat ID - GetUnreadCount(ctx context.Context, chatId ChatId) (uint32, error) - - // SetMuteState updates the mute state for a chat - SetMuteState(ctx context.Context, chatId ChatId, isMuted bool) error - - // SetSubscriptionState updates the subscription state for a chat - SetSubscriptionState(ctx context.Context, chatId ChatId, isSubscribed bool) error -} diff --git a/pkg/code/data/chat/tests/tests.go b/pkg/code/data/chat/tests/tests.go deleted file mode 100644 index f9eaaadf..00000000 --- a/pkg/code/data/chat/tests/tests.go +++ /dev/null @@ -1,445 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/chat" -) - -func RunTests(t *testing.T, s chat.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s chat.Store){ - testChatRoundTrip, - testMessageRoundTrip, - testAdvancePointer, - testGetUnreadCount, - testMuteState, - tesSubscriptionState, - testGetAllChatsByUserPaging, - testGetAllMessagesByChatPaging, - } { - tf(t, s) - teardown() - } -} - -func testChatRoundTrip(t *testing.T, s chat.Store) { - t.Run("testChatRoundTrip", func(t *testing.T) { - ctx := context.Background() - - chatId := chat.GetChatId("user", "domain", true) - - _, err := s.GetChatById(ctx, chatId) - assert.Equal(t, chat.ErrChatNotFound, err) - - _, err = s.GetAllChatsForUser(ctx, "user", nil, query.Ascending, 10) - assert.Equal(t, chat.ErrChatNotFound, err) - - expected := &chat.Chat{ - ChatId: chatId, - ChatType: chat.ChatTypeExternalApp, - ChatTitle: "domain", - IsVerified: true, - - CodeUser: "user", - - ReadPointer: pointer.String("msg123"), - IsMuted: true, - IsUnsubscribed: true, - - CreatedAt: time.Now(), - } - cloned := expected.Clone() - - require.NoError(t, s.PutChat(ctx, expected)) - assert.True(t, expected.Id > 0) - - actual, err := s.GetChatById(ctx, chatId) - require.NoError(t, err) - assertEquivalentChatRecords(t, &cloned, actual) - - chats, err := s.GetAllChatsForUser(ctx, "user", nil, query.Ascending, 10) - require.NoError(t, err) - require.Len(t, chats, 1) - assertEquivalentChatRecords(t, &cloned, chats[0]) - - assert.Equal(t, chat.ErrChatAlreadyExists, s.PutChat(ctx, expected)) - }) -} - -func testMessageRoundTrip(t *testing.T, s chat.Store) { - t.Run("testMessageRoundTrip", func(t *testing.T) { - ctx := context.Background() - - messageId := "message_id" - - for i := 0; i < 3; i++ { - chatId := chat.GetChatId("sender", fmt.Sprintf("receiver%d", i), true) - - _, err := s.GetMessageById(ctx, chatId, messageId) - assert.Equal(t, chat.ErrMessageNotFound, err) - - _, err = s.GetAllMessagesByChat(ctx, chatId, nil, query.Ascending, 10) - assert.Equal(t, chat.ErrMessageNotFound, err) - - expected := &chat.Message{ - ChatId: chatId, - - MessageId: messageId, - Data: []byte("message"), - - IsSilent: true, - ContentLength: 3, - - Timestamp: time.Now(), - } - cloned := expected.Clone() - - require.NoError(t, s.PutMessage(ctx, expected)) - assert.True(t, expected.Id > 0) - - actual, err := s.GetMessageById(ctx, chatId, messageId) - require.NoError(t, err) - assertEquivalentMessageRecords(t, &cloned, actual) - - messages, err := s.GetAllMessagesByChat(ctx, chatId, nil, query.Ascending, 10) - require.NoError(t, err) - require.Len(t, messages, 1) - assertEquivalentMessageRecords(t, &cloned, messages[0]) - - assert.Equal(t, chat.ErrMessageAlreadyExists, s.PutMessage(ctx, expected)) - } - - for i := 0; i < 3; i++ { - chatId := chat.GetChatId("sender", fmt.Sprintf("receiver%d", i), true) - - require.NoError(t, s.DeleteMessage(ctx, chatId, messageId)) - - _, err := s.GetMessageById(ctx, chatId, messageId) - assert.Equal(t, chat.ErrMessageNotFound, err) - - _, err = s.GetAllMessagesByChat(ctx, chatId, nil, query.Ascending, 10) - assert.Equal(t, chat.ErrMessageNotFound, err) - - if i == 0 { - chatId := chat.GetChatId("sender", "receiver1", true) - - _, err := s.GetMessageById(ctx, chatId, messageId) - require.NoError(t, err) - - _, err = s.GetAllMessagesByChat(ctx, chatId, nil, query.Ascending, 10) - require.NoError(t, err) - } - - require.NoError(t, s.DeleteMessage(ctx, chatId, messageId)) - } - }) -} - -func testAdvancePointer(t *testing.T, s chat.Store) { - t.Run("testAdvancePointer", func(t *testing.T) { - ctx := context.Background() - - chatId := chat.GetChatId("user", "domain", true) - - assert.Equal(t, chat.ErrChatNotFound, s.AdvancePointer(ctx, chatId, "pointer")) - - record := &chat.Chat{ - ChatId: chatId, - ChatType: chat.ChatTypeExternalApp, - ChatTitle: "domain", - IsVerified: true, - - CodeUser: "user", - - CreatedAt: time.Now(), - } - require.NoError(t, s.PutChat(ctx, record)) - - require.NoError(t, s.AdvancePointer(ctx, chatId, "pointer")) - - actual, err := s.GetChatById(ctx, chatId) - require.NoError(t, err) - require.NotNil(t, actual.ReadPointer) - assert.Equal(t, "pointer", *actual.ReadPointer) - }) -} - -func testGetUnreadCount(t *testing.T, s chat.Store) { - t.Run("testGetUnreadCount", func(t *testing.T) { - ctx := context.Background() - - chatId := chat.GetChatId("user", "domain", true) - - count, err := s.GetUnreadCount(ctx, chatId) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - chatRecord := &chat.Chat{ - ChatId: chatId, - ChatType: chat.ChatTypeExternalApp, - ChatTitle: "domain", - IsVerified: true, - - CodeUser: "user", - - CreatedAt: time.Now(), - } - require.NoError(t, s.PutChat(ctx, chatRecord)) - - for i := 1; i <= 3; i++ { - for j, isSilent := range []bool{false, true} { - contentLength := uint8(i) - if isSilent { - contentLength *= 10 - } - messageRecord := &chat.Message{ - ChatId: chatId, - - MessageId: fmt.Sprintf("message%d%d", i, j), - Data: []byte("message"), - - IsSilent: isSilent, - ContentLength: contentLength, - - Timestamp: time.Now().Add(time.Duration(i) * time.Second), - } - require.NoError(t, s.PutMessage(ctx, messageRecord)) - } - } - - count, err = s.GetUnreadCount(ctx, chatId) - require.NoError(t, err) - assert.EqualValues(t, 3, count) - - var deltaRead int - for i := 1; i <= 3; i++ { - deltaRead += 1 - - for j := 0; j < 2; j++ { - require.NoError(t, s.AdvancePointer(ctx, chatId, fmt.Sprintf("message%d%d", i, j))) - - count, err = s.GetUnreadCount(ctx, chatId) - require.NoError(t, err) - assert.EqualValues(t, 3-deltaRead, count) - } - } - }) -} - -func testMuteState(t *testing.T, s chat.Store) { - t.Run("testMuteState", func(t *testing.T) { - ctx := context.Background() - - chatId := chat.GetChatId("user", "domain", true) - - require.NoError(t, s.PutChat(ctx, &chat.Chat{ - ChatId: chatId, - ChatType: chat.ChatTypeExternalApp, - ChatTitle: "domain", - IsVerified: true, - - CodeUser: "user", - - IsMuted: false, - - CreatedAt: time.Now(), - })) - - for _, expected := range []bool{false, true, false} { - require.NoError(t, s.SetMuteState(ctx, chatId, expected)) - - actual, err := s.GetChatById(ctx, chatId) - require.NoError(t, err) - assert.Equal(t, expected, actual.IsMuted) - } - }) -} - -func tesSubscriptionState(t *testing.T, s chat.Store) { - t.Run("tesSubscriptionState", func(t *testing.T) { - ctx := context.Background() - - chatId := chat.GetChatId("user", "domain", true) - - require.NoError(t, s.PutChat(ctx, &chat.Chat{ - ChatId: chatId, - ChatType: chat.ChatTypeExternalApp, - ChatTitle: "domain", - IsVerified: true, - - CodeUser: "user", - - IsUnsubscribed: false, - - CreatedAt: time.Now(), - })) - - for _, expected := range []bool{false, true, false} { - require.NoError(t, s.SetSubscriptionState(ctx, chatId, expected)) - - actual, err := s.GetChatById(ctx, chatId) - require.NoError(t, err) - assert.Equal(t, !expected, actual.IsUnsubscribed) - } - }) -} - -func testGetAllChatsByUserPaging(t *testing.T, s chat.Store) { - t.Run("testGetAllChatsByUserPaging", func(t *testing.T) { - ctx := context.Background() - - user := "user" - - var expected []*chat.Chat - for i := 0; i < 10; i++ { - merchant := fmt.Sprintf("merchant%d.com", i) - - record := &chat.Chat{ - ChatId: chat.GetChatId(merchant, user, true), - ChatType: chat.ChatTypeExternalApp, - ChatTitle: merchant, - IsVerified: true, - CodeUser: user, - CreatedAt: time.Now(), - } - require.NoError(t, s.PutChat(ctx, record)) - expected = append(expected, record) - } - - actual, err := s.GetAllChatsForUser(ctx, user, query.ToCursor(0), query.Ascending, 100) - require.NoError(t, err) - require.Len(t, actual, len(expected)) - for i := 0; i < len(expected); i++ { - assertEquivalentChatRecords(t, expected[i], actual[i]) - } - - actual, err = s.GetAllChatsForUser(ctx, user, query.ToCursor(1000), query.Descending, 100) - require.NoError(t, err) - require.Len(t, actual, len(expected)) - for i := 0; i < len(expected); i++ { - assertEquivalentChatRecords(t, expected[len(expected)-i-1], actual[i]) - } - - actual, err = s.GetAllChatsForUser(ctx, user, query.ToCursor(expected[1].Id), query.Ascending, 3) - require.NoError(t, err) - require.Len(t, actual, 3) - assertEquivalentChatRecords(t, expected[2], actual[0]) - assertEquivalentChatRecords(t, expected[3], actual[1]) - assertEquivalentChatRecords(t, expected[4], actual[2]) - - actual, err = s.GetAllChatsForUser(ctx, user, query.ToCursor(expected[5].Id), query.Descending, 2) - require.NoError(t, err) - require.Len(t, actual, 2) - assertEquivalentChatRecords(t, expected[4], actual[0]) - assertEquivalentChatRecords(t, expected[3], actual[1]) - - _, err = s.GetAllChatsForUser(ctx, user, query.ToCursor(1000), query.Ascending, 100) - assert.Equal(t, chat.ErrChatNotFound, err) - - _, err = s.GetAllChatsForUser(ctx, user, query.ToCursor(0), query.Descending, 100) - assert.Equal(t, chat.ErrChatNotFound, err) - }) -} - -func testGetAllMessagesByChatPaging(t *testing.T, s chat.Store) { - t.Run("testGetAllMessagesByChatPaging", func(t *testing.T) { - ctx := context.Background() - - start := time.Now() - - for i, useSimilarTimestamps := range []bool{true, false} { - chatId := chat.GetChatId(fmt.Sprintf("merchant%d", i), "user", true) - - var expected []*chat.Message - for j := 0; j < 10; j++ { - record := &chat.Message{ - ChatId: chatId, - - MessageId: base58.Encode([]byte(fmt.Sprintf("message%d", j))), - Data: []byte("data"), - - ContentLength: 1, - - Timestamp: start, - } - if !useSimilarTimestamps { - record.Timestamp = start.Add(time.Duration(j) * time.Minute) - } - require.NoError(t, s.PutMessage(ctx, record)) - expected = append(expected, record) - } - - actual, err := s.GetAllMessagesByChat(ctx, chatId, nil, query.Ascending, 100) - require.NoError(t, err) - require.Len(t, actual, len(expected)) - for i := 0; i < len(expected); i++ { - assertEquivalentMessageRecords(t, expected[i], actual[i]) - } - - actual, err = s.GetAllMessagesByChat(ctx, chatId, nil, query.Descending, 100) - require.NoError(t, err) - require.Len(t, actual, len(expected)) - for i := 0; i < len(expected); i++ { - assertEquivalentMessageRecords(t, expected[len(expected)-i-1], actual[i]) - } - - actual, err = s.GetAllMessagesByChat(ctx, chatId, getMessageCursor(t, expected[1]), query.Ascending, 3) - require.NoError(t, err) - require.Len(t, actual, 3) - assertEquivalentMessageRecords(t, expected[2], actual[0]) - assertEquivalentMessageRecords(t, expected[3], actual[1]) - assertEquivalentMessageRecords(t, expected[4], actual[2]) - - actual, err = s.GetAllMessagesByChat(ctx, chatId, getMessageCursor(t, expected[5]), query.Descending, 2) - require.NoError(t, err) - require.Len(t, actual, 2) - assertEquivalentMessageRecords(t, expected[4], actual[0]) - assertEquivalentMessageRecords(t, expected[3], actual[1]) - - _, err = s.GetAllMessagesByChat(ctx, chatId, getMessageCursor(t, expected[len(expected)-1]), query.Ascending, 100) - assert.Equal(t, chat.ErrMessageNotFound, err) - - _, err = s.GetAllMessagesByChat(ctx, chatId, getMessageCursor(t, expected[0]), query.Descending, 100) - assert.Equal(t, chat.ErrMessageNotFound, err) - - _, err = s.GetAllMessagesByChat(ctx, chatId, []byte("does-not-exist"), query.Ascending, 100) - assert.Equal(t, chat.ErrInvalidMessageCursor, err) - } - }) -} - -func assertEquivalentChatRecords(t *testing.T, obj1, obj2 *chat.Chat) { - assert.Equal(t, obj1.ChatId, obj2.ChatId) - assert.Equal(t, obj1.ChatType, obj2.ChatType) - assert.Equal(t, obj1.ChatTitle, obj2.ChatTitle) - assert.Equal(t, obj1.IsVerified, obj2.IsVerified) - assert.Equal(t, obj1.CodeUser, obj2.CodeUser) - assert.Equal(t, obj1.ReadPointer, obj2.ReadPointer) - assert.Equal(t, obj1.IsMuted, obj2.IsMuted) - assert.Equal(t, obj1.IsUnsubscribed, obj2.IsUnsubscribed) - assert.Equal(t, obj1.CreatedAt.Unix(), obj2.CreatedAt.Unix()) -} - -func assertEquivalentMessageRecords(t *testing.T, obj1, obj2 *chat.Message) { - assert.Equal(t, obj1.ChatId, obj2.ChatId) - assert.Equal(t, obj1.MessageId, obj2.MessageId) - assert.EqualValues(t, obj1.Data, obj2.Data) - assert.Equal(t, obj1.IsSilent, obj2.IsSilent) - assert.Equal(t, obj1.ContentLength, obj2.ContentLength) - assert.Equal(t, obj1.Timestamp.Unix(), obj2.Timestamp.Unix()) -} - -func getMessageCursor(t *testing.T, record *chat.Message) query.Cursor { - decoded, err := base58.Decode(record.MessageId) - require.NoError(t, err) - return query.Cursor(decoded) -} diff --git a/pkg/code/data/commitment/commitment.go b/pkg/code/data/commitment/commitment.go deleted file mode 100644 index bf2ded5b..00000000 --- a/pkg/code/data/commitment/commitment.go +++ /dev/null @@ -1,173 +0,0 @@ -package commitment - -import ( - "errors" - "time" - - "github.com/code-payments/code-server/pkg/pointer" -) - -type State uint8 - -const ( - StateUnknown State = iota - StatePayingDestination - StateReadyToOpen // No longer valid in the CVM - StateOpening // No longer valid in the CVM - StateOpen - StateClosing - StateClosed - - // Theoretical states that don't have any logic right now, but will be needed - // later for GC. - StateReadyToRemoveFromMerkleTree - StateRemovedFromMerkleTree -) - -type Record struct { - Id uint64 - - Address string - VaultAddress string - - Pool string - RecentRoot string - - Transcript string - Destination string - Amount uint64 - - Intent string - ActionId uint32 - - Owner string - - // Has the treasury been repaid for advancing Record.Amount to Record.Destination? - // Not to be confused with payments being diverted to this commitment and then - // being closed. - TreasuryRepaid bool - // The commitment where repayment for Record.Amount will be diverted to. - RepaymentDivertedTo *string - - State State - - CreatedAt time.Time -} - -func (r *Record) Validate() error { - if len(r.Address) == 0 { - return errors.New("address is required") - } - - if len(r.VaultAddress) == 0 { - return errors.New("vault address is required") - } - - if len(r.Pool) == 0 { - return errors.New("pool is required") - } - - if len(r.RecentRoot) == 0 { - return errors.New("recent root is required") - } - - if len(r.Transcript) == 0 { - return errors.New("transcript is required") - } - - if len(r.Destination) == 0 { - return errors.New("destination is required") - } - - if r.Amount == 0 { - return errors.New("settlement amount must be positive") - } - - if len(r.Intent) == 0 { - return errors.New("intent is required") - } - - if len(r.Owner) == 0 { - return errors.New("owner is required") - } - - return nil -} - -func (r *Record) Clone() *Record { - return &Record{ - Id: r.Id, - - Address: r.Address, - VaultAddress: r.VaultAddress, - - Pool: r.Pool, - RecentRoot: r.RecentRoot, - - Transcript: r.Transcript, - Destination: r.Destination, - Amount: r.Amount, - - Intent: r.Intent, - ActionId: r.ActionId, - - Owner: r.Owner, - - TreasuryRepaid: r.TreasuryRepaid, - RepaymentDivertedTo: pointer.StringCopy(r.RepaymentDivertedTo), - - State: r.State, - - CreatedAt: r.CreatedAt, - } -} - -func (r *Record) CopyTo(dst *Record) { - dst.Id = r.Id - - dst.Address = r.Address - dst.VaultAddress = r.VaultAddress - - dst.Pool = r.Pool - dst.RecentRoot = r.RecentRoot - - dst.Transcript = r.Transcript - dst.Destination = r.Destination - dst.Amount = r.Amount - - dst.Intent = r.Intent - dst.ActionId = r.ActionId - - dst.Owner = r.Owner - - dst.TreasuryRepaid = r.TreasuryRepaid - dst.RepaymentDivertedTo = pointer.StringCopy(r.RepaymentDivertedTo) - - dst.State = r.State - - dst.CreatedAt = r.CreatedAt -} - -func (s State) String() string { - switch s { - case StateUnknown: - return "unknown" - case StatePayingDestination: - return "paying_destination" - case StateReadyToOpen: - return "ready_to_open" - case StateOpening: - return "opening" - case StateOpen: - return "open" - case StateClosing: - return "closing" - case StateClosed: - return "closed" - case StateReadyToRemoveFromMerkleTree: - return "ready_to_remove_from_merkle_tree" - case StateRemovedFromMerkleTree: - return "removed_from_merkle_tree" - } - return "unknown" -} diff --git a/pkg/code/data/commitment/memory/store.go b/pkg/code/data/commitment/memory/store.go deleted file mode 100644 index 8261036d..00000000 --- a/pkg/code/data/commitment/memory/store.go +++ /dev/null @@ -1,359 +0,0 @@ -package memory - -import ( - "context" - "sort" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/database/query" -) - -type ById []*commitment.Record - -func (a ById) Len() int { return len(a) } -func (a ById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ById) Less(i, j int) bool { return a[i].Id < a[j].Id } - -type store struct { - mu sync.Mutex - records []*commitment.Record - last uint64 -} - -// New returns a new in memory commitment.Store -func New() commitment.Store { - return &store{} -} - -// Save implements commitment.Store.Save -func (s *store) Save(_ context.Context, data *commitment.Record) error { - if err := data.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - if item := s.find(data); item != nil { - if item.TreasuryRepaid && !data.TreasuryRepaid { - return commitment.ErrInvalidCommitment - } - - if item.RepaymentDivertedTo != nil && data.RepaymentDivertedTo == nil { - return commitment.ErrInvalidCommitment - } - - if item.RepaymentDivertedTo != nil && data.RepaymentDivertedTo != nil && *item.RepaymentDivertedTo != *data.RepaymentDivertedTo { - return commitment.ErrInvalidCommitment - } - - if item.State > data.State { - return commitment.ErrInvalidCommitment - } - - item.TreasuryRepaid = data.TreasuryRepaid - item.RepaymentDivertedTo = data.RepaymentDivertedTo - item.State = data.State - - item.CopyTo(data) - } else { - if data.Id == 0 { - data.Id = s.last - } - if data.CreatedAt.IsZero() { - data.CreatedAt = time.Now() - } - s.records = append(s.records, data.Clone()) - } - - return nil -} - -// GetByAddress implements commitment.Store.GetByAddress -func (s *store) GetByAddress(_ context.Context, address string) (*commitment.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if item := s.findByAddress(address); item != nil { - return item.Clone(), nil - } - - return nil, commitment.ErrCommitmentNotFound -} - -// GetByVault implements commitment.Store.GetByVault -func (s *store) GetByVault(_ context.Context, address string) (*commitment.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if item := s.findByVault(address); item != nil { - return item.Clone(), nil - } - - return nil, commitment.ErrCommitmentNotFound -} - -// GetByAction implements commitment.Store.GetByAction -func (s *store) GetByAction(_ context.Context, intentId string, actionId uint32) (*commitment.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if item := s.findByAction(intentId, actionId); item != nil { - return item.Clone(), nil - } - - return nil, commitment.ErrCommitmentNotFound -} - -// GetAllByState implements commitment.Store.GetAllByState -func (s *store) GetAllByState(ctx context.Context, state commitment.State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*commitment.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if items := s.findByState(state); len(items) > 0 { - res := s.filter(items, cursor, limit, direction) - - if len(res) == 0 { - return nil, commitment.ErrCommitmentNotFound - } - - return res, nil - } - - return nil, commitment.ErrCommitmentNotFound -} - -// GetUpgradeableByOwner implements commitment.Store.GetUpgradeableByOwner -func (s *store) GetUpgradeableByOwner(_ context.Context, owner string, limit uint64) ([]*commitment.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByOwner(owner) - items = s.filterByNilRepaymentDivertedTo(items) - items = s.filterByStates( - items, - false, - commitment.StateUnknown, - commitment.StatePayingDestination, - commitment.StateReadyToRemoveFromMerkleTree, - commitment.StateRemovedFromMerkleTree, - ) - - if len(items) > int(limit) { - items = items[:limit] - } - - if len(items) == 0 { - return nil, commitment.ErrCommitmentNotFound - } - return items, nil -} - -// GetUsedTreasuryPoolDeficit implements commitment.Store.GetUsedTreasuryPoolDeficit -func (s *store) GetUsedTreasuryPoolDeficit(_ context.Context, pool string) (uint64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByPool(pool) - items = s.filterByStates(items, false, commitment.StateUnknown) - items = s.filterByRepaymentStatus(items, false) - return s.sumQuantity(items), nil -} - -// GetTotalTreasuryPoolDeficit implements commitment.Store.GetTotalTreasuryPoolDeficit -func (s *store) GetTotalTreasuryPoolDeficit(_ context.Context, pool string) (uint64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByPool(pool) - items = s.filterByRepaymentStatus(items, false) - return s.sumQuantity(items), nil -} - -// CountByState implements commitment.Store.CountByState -func (s *store) CountByState(_ context.Context, state commitment.State) (uint64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByState(state) - return uint64(len(items)), nil -} - -// CountPendingRepaymentsDivertedToCommitment implements commitment.Store.CountPendingRepaymentsDivertedToCommitment -func (s *store) CountPendingRepaymentsDivertedToCommitment(_ context.Context, address string) (uint64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByRepaymentsDivertedToCommitment(address) - items = s.filterByRepaymentStatus(items, false) - return uint64(len(items)), nil -} - -func (s *store) find(data *commitment.Record) *commitment.Record { - for _, item := range s.records { - if item.Id == data.Id { - return item - } - if data.Address == item.Address { - return item - } - } - return nil -} - -func (s *store) findByAddress(address string) *commitment.Record { - for _, item := range s.records { - if item.Address == address { - return item - } - } - return nil -} - -func (s *store) findByVault(address string) *commitment.Record { - for _, item := range s.records { - if item.VaultAddress == address { - return item - } - } - return nil -} - -func (s *store) findByAction(intentId string, actionId uint32) *commitment.Record { - for _, item := range s.records { - if item.Intent == intentId && item.ActionId == actionId { - return item - } - } - return nil -} - -func (s *store) findByPool(pool string) []*commitment.Record { - var res []*commitment.Record - for _, item := range s.records { - if item.Pool == pool { - res = append(res, item) - } - } - return res -} - -func (s *store) findByRepaymentsDivertedToCommitment(address string) []*commitment.Record { - var res []*commitment.Record - for _, item := range s.records { - if item.RepaymentDivertedTo != nil && *item.RepaymentDivertedTo == address { - res = append(res, item) - } - } - return res -} - -func (s *store) findByState(state commitment.State) []*commitment.Record { - res := make([]*commitment.Record, 0) - for _, item := range s.records { - if item.State == state { - res = append(res, item) - continue - } - } - return res -} - -func (s *store) findByOwner(owner string) []*commitment.Record { - res := make([]*commitment.Record, 0) - for _, item := range s.records { - if item.Owner == owner { - res = append(res, item) - } - } - return res -} - -func (s *store) filterByStates(items []*commitment.Record, include bool, states ...commitment.State) []*commitment.Record { - var res []*commitment.Record - for _, item := range items { - var inState bool - for _, state := range states { - if item.State == state { - inState = true - break - } - } - - if (include && inState) || (!include && !inState) { - res = append(res, item) - } - } - return res -} - -func (s *store) filterByRepaymentStatus(items []*commitment.Record, want bool) []*commitment.Record { - var res []*commitment.Record - for _, item := range items { - if item.TreasuryRepaid == want { - res = append(res, item) - } - } - return res -} - -func (s *store) filterByNilRepaymentDivertedTo(items []*commitment.Record) []*commitment.Record { - var res []*commitment.Record - for _, item := range items { - if item.RepaymentDivertedTo == nil { - res = append(res, item) - } - } - return res -} - -func (s *store) filter(items []*commitment.Record, cursor query.Cursor, limit uint64, direction query.Ordering) []*commitment.Record { - var start uint64 - - start = 0 - if direction == query.Descending { - start = s.last + 1 - } - if len(cursor) > 0 { - start = cursor.ToUint64() - } - - var res []*commitment.Record - for _, item := range items { - if item.Id > start && direction == query.Ascending { - res = append(res, item) - } - if item.Id < start && direction == query.Descending { - res = append(res, item) - } - } - - if direction == query.Descending { - sort.Sort(sort.Reverse(ById(res))) - } - - if len(res) >= int(limit) { - return res[:limit] - } - - return res -} - -func (s *store) sumQuantity(items []*commitment.Record) uint64 { - var res uint64 - for _, item := range items { - res += item.Amount - } - return res -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.records = nil - s.last = 0 -} diff --git a/pkg/code/data/commitment/memory/store_test.go b/pkg/code/data/commitment/memory/store_test.go deleted file mode 100644 index 6d0270b2..00000000 --- a/pkg/code/data/commitment/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/commitment/tests" -) - -func TestCommitmentMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/commitment/postgres/model.go b/pkg/code/data/commitment/postgres/model.go deleted file mode 100644 index 295e0aa2..00000000 --- a/pkg/code/data/commitment/postgres/model.go +++ /dev/null @@ -1,328 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - q "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/pointer" -) - -const ( - tableName = "codewallet__core_commitment" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - - Address string `db:"address"` - VaultAddress string `db:"vault"` - - Pool string `db:"pool"` - RecentRoot string `db:"recent_root"` - - Transcript string `db:"transcript"` - Destination string `db:"destination"` - Amount uint64 `db:"amount"` - - Intent string `db:"intent"` - ActionId uint `db:"action_id"` - - Owner string `db:"owner"` - - TreasuryRepaid bool `db:"treasury_repaid"` - RepaymentDivertedTo sql.NullString `db:"repayment_diverted_to"` - - State uint `db:"state"` - - CreatedAt time.Time `db:"created_at"` -} - -func toModel(obj *commitment.Record) (*model, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &model{ - Address: obj.Address, - VaultAddress: obj.VaultAddress, - - Pool: obj.Pool, - RecentRoot: obj.RecentRoot, - - Transcript: obj.Transcript, - Destination: obj.Destination, - Amount: obj.Amount, - - Intent: obj.Intent, - ActionId: uint(obj.ActionId), - - Owner: obj.Owner, - - TreasuryRepaid: obj.TreasuryRepaid, - RepaymentDivertedTo: sql.NullString{ - Valid: obj.RepaymentDivertedTo != nil, - String: *pointer.StringOrDefault(obj.RepaymentDivertedTo, ""), - }, - - State: uint(obj.State), - - CreatedAt: obj.CreatedAt, - }, nil -} - -func fromModel(obj *model) *commitment.Record { - return &commitment.Record{ - Id: uint64(obj.Id.Int64), - - Address: obj.Address, - VaultAddress: obj.VaultAddress, - - Pool: obj.Pool, - RecentRoot: obj.RecentRoot, - - Transcript: obj.Transcript, - Destination: obj.Destination, - Amount: obj.Amount, - - Intent: obj.Intent, - ActionId: uint32(obj.ActionId), - - Owner: obj.Owner, - - TreasuryRepaid: obj.TreasuryRepaid, - RepaymentDivertedTo: pointer.StringIfValid(obj.RepaymentDivertedTo.Valid, obj.RepaymentDivertedTo.String), - - State: commitment.State(obj.State), - - CreatedAt: obj.CreatedAt, - } -} - -func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - divertedToCondition := tableName + ".repayment_diverted_to IS NULL" - if m.RepaymentDivertedTo.Valid { - divertedToCondition = "(" + tableName + ".repayment_diverted_to IS NULL OR " + tableName + ".repayment_diverted_to = $12)" - } - - treasuryRepaidCondition := tableName + ".treasury_repaid IS FALSE" - if m.TreasuryRepaid { - treasuryRepaidCondition = `TRUE` - } - - // If you're wondering why this is so safeguarded, commitments are at a - // high risk of experiencing race conditions without distributed locks. - // Luckily, all updateable state-like fields should progress forward in a - // predictable manner, making conditions easy to reason about. - query := `INSERT INTO ` + tableName + ` - (address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) - - ON CONFLICT (address) - DO UPDATE - SET treasury_repaid = $11 OR ` + tableName + `.treasury_repaid , repayment_diverted_to = COALESCE($12, ` + tableName + `.repayment_diverted_to), state = GREATEST($13, ` + tableName + `.state) - WHERE ` + tableName + `.address = $1 AND ` + treasuryRepaidCondition + ` AND ` + divertedToCondition + ` AND ` + tableName + `.state <= $13 - - RETURNING - id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at` - - if m.CreatedAt.IsZero() { - m.CreatedAt = time.Now() - } - - err := tx.QueryRowxContext( - ctx, - query, - m.Address, - m.VaultAddress, - m.Pool, - m.RecentRoot, - m.Transcript, - m.Destination, - m.Amount, - m.Intent, - m.ActionId, - m.Owner, - m.TreasuryRepaid, - m.RepaymentDivertedTo, - m.State, - m.CreatedAt.UTC(), - ).StructScan(m) - - return pgutil.CheckNoRows(err, commitment.ErrInvalidCommitment) - }) -} - -func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*model, error) { - res := &model{} - - query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at - FROM ` + tableName + ` - WHERE address = $1 - LIMIT 1` - - err := db.GetContext(ctx, res, query, address) - if err != nil { - return nil, pgutil.CheckNoRows(err, commitment.ErrCommitmentNotFound) - } - return res, nil -} - -func dbGetByVault(ctx context.Context, db *sqlx.DB, address string) (*model, error) { - res := &model{} - - query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at - FROM ` + tableName + ` - WHERE vault = $1 - LIMIT 1` - - err := db.GetContext(ctx, res, query, address) - if err != nil { - return nil, pgutil.CheckNoRows(err, commitment.ErrCommitmentNotFound) - } - return res, nil -} - -func dbGetByAction(ctx context.Context, db *sqlx.DB, intentId string, actionId uint32) (*model, error) { - res := &model{} - - query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at - FROM ` + tableName + ` - WHERE intent = $1 AND action_id = $2 - LIMIT 1` - - err := db.GetContext(ctx, res, query, intentId, actionId) - if err != nil { - return nil, pgutil.CheckNoRows(err, commitment.ErrCommitmentNotFound) - } - return res, nil -} - -func dbGetAllByState(ctx context.Context, db *sqlx.DB, state commitment.State, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*model, error) { - res := []*model{} - - query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at - FROM ` + tableName + ` - WHERE (state = $1) - ` - - opts := []interface{}{state} - query, opts = q.PaginateQuery(query, opts, cursor, limit, direction) - - err := db.SelectContext(ctx, &res, query, opts...) - if err != nil { - return nil, pgutil.CheckNoRows(err, commitment.ErrCommitmentNotFound) - } - - if len(res) == 0 { - return nil, commitment.ErrCommitmentNotFound - } - return res, nil -} - -func dbGetUpgradeableByOwner(ctx context.Context, db *sqlx.DB, owner string, limit uint64) ([]*model, error) { - res := []*model{} - - query := `SELECT id, address, vault, pool, recent_root, transcript, destination, amount, intent, action_id, owner, treasury_repaid, repayment_diverted_to, state, created_at - FROM ` + tableName + ` - WHERE owner = $1 AND state > $3 AND state < $4 AND repayment_diverted_to IS NULL - LIMIT $2 - ` - - err := db.SelectContext(ctx, &res, query, owner, limit, commitment.StatePayingDestination, commitment.StateReadyToRemoveFromMerkleTree) - if err != nil { - return nil, pgutil.CheckNoRows(err, commitment.ErrCommitmentNotFound) - } - - if len(res) == 0 { - return nil, commitment.ErrCommitmentNotFound - } - return res, nil -} - -func dbGetUsedTreasuryPoolDeficit(ctx context.Context, db *sqlx.DB, pool string) (uint64, error) { - var res uint64 - - query := `SELECT COALESCE(SUM(amount), 0) FROM ` + tableName + ` - WHERE pool = $1 AND NOT treasury_repaid AND state != $2 - ` - - err := db.GetContext( - ctx, - &res, - query, - pool, - commitment.StateUnknown, - ) - if err != nil { - return 0, err - } - - return res, nil -} - -func dbGetTotalTreasuryPoolDeficit(ctx context.Context, db *sqlx.DB, pool string) (uint64, error) { - var res uint64 - - query := `SELECT COALESCE(SUM(amount), 0) FROM ` + tableName + ` - WHERE pool = $1 AND NOT treasury_repaid - ` - - err := db.GetContext( - ctx, - &res, - query, - pool, - ) - if err != nil { - return 0, err - } - - return res, nil -} - -func dbCountByState(ctx context.Context, db *sqlx.DB, state commitment.State) (uint64, error) { - var res uint64 - - query := `SELECT COUNT(*) FROM ` + tableName + ` - WHERE state = $1 - ` - - err := db.GetContext( - ctx, - &res, - query, - state, - ) - if err != nil { - return 0, err - } - - return res, nil -} - -func dbCountPendingRepaymentsDivertedToCommitment(ctx context.Context, db *sqlx.DB, commitment string) (uint64, error) { - var res uint64 - - query := `SELECT COUNT(*) FROM ` + tableName + ` - WHERE repayment_diverted_to = $1 AND NOT treasury_repaid - ` - - err := db.GetContext( - ctx, - &res, - query, - commitment, - ) - if err != nil { - return 0, err - } - - return res, nil -} diff --git a/pkg/code/data/commitment/postgres/store.go b/pkg/code/data/commitment/postgres/store.go deleted file mode 100644 index 1f9f17a4..00000000 --- a/pkg/code/data/commitment/postgres/store.go +++ /dev/null @@ -1,117 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/database/query" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new in memory commitment.Store -func New(db *sql.DB) commitment.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Save implements commitment.Store.Save -func (s *store) Save(ctx context.Context, record *commitment.Record) error { - model, err := toModel(record) - if err != nil { - return err - } - - if err := model.dbSave(ctx, s.db); err != nil { - return err - } - - res := fromModel(model) - res.CopyTo(record) - - return nil -} - -// GetByAddress implements commitment.Store.GetByAddress -func (s *store) GetByAddress(ctx context.Context, address string) (*commitment.Record, error) { - model, err := dbGetByAddress(ctx, s.db, address) - if err != nil { - return nil, err - } - - return fromModel(model), nil -} - -// GetByVault implements commitment.Store.GetByVault -func (s *store) GetByVault(ctx context.Context, address string) (*commitment.Record, error) { - model, err := dbGetByVault(ctx, s.db, address) - if err != nil { - return nil, err - } - - return fromModel(model), nil -} - -// GetByAction implements commitment.Store.GetByAction -func (s *store) GetByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) { - model, err := dbGetByAction(ctx, s.db, intentId, actionId) - if err != nil { - return nil, err - } - - return fromModel(model), nil -} - -// GetAllByState implements commitment.Store.GetAllByState -func (s *store) GetAllByState(ctx context.Context, state commitment.State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*commitment.Record, error) { - models, err := dbGetAllByState(ctx, s.db, state, cursor, limit, direction) - if err != nil { - return nil, err - } - - res := make([]*commitment.Record, len(models)) - for i, model := range models { - res[i] = fromModel(model) - } - return res, nil -} - -// GetUpgradeableByOwner implements commitment.Store.GetUpgradeableByOwner -func (s *store) GetUpgradeableByOwner(ctx context.Context, owner string, limit uint64) ([]*commitment.Record, error) { - models, err := dbGetUpgradeableByOwner(ctx, s.db, owner, limit) - if err != nil { - return nil, err - } - - res := make([]*commitment.Record, len(models)) - for i, model := range models { - res[i] = fromModel(model) - } - return res, nil -} - -// GetUsedTreasuryPoolDeficit implements commitment.Store.GetUsedTreasuryPoolDeficit -func (s *store) GetUsedTreasuryPoolDeficit(ctx context.Context, pool string) (uint64, error) { - return dbGetUsedTreasuryPoolDeficit(ctx, s.db, pool) -} - -// GetTotalTreasuryPoolDeficit implements commitment.Store.GetTotalTreasuryPoolDeficit -func (s *store) GetTotalTreasuryPoolDeficit(ctx context.Context, pool string) (uint64, error) { - return dbGetTotalTreasuryPoolDeficit(ctx, s.db, pool) -} - -// CountByState implements commitment.Store.CountByState -func (s *store) CountByState(ctx context.Context, state commitment.State) (uint64, error) { - return dbCountByState(ctx, s.db, state) -} - -// CountPendingRepaymentsDivertedToCommitment implements commitment.Store.CountPendingRepaymentsDivertedToCommitment -func (s *store) CountPendingRepaymentsDivertedToCommitment(ctx context.Context, address string) (uint64, error) { - return dbCountPendingRepaymentsDivertedToCommitment(ctx, s.db, address) -} diff --git a/pkg/code/data/commitment/postgres/store_test.go b/pkg/code/data/commitment/postgres/store_test.go deleted file mode 100644 index de8fdb5c..00000000 --- a/pkg/code/data/commitment/postgres/store_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/commitment/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_commitment( - id SERIAL NOT NULL PRIMARY KEY, - - address TEXT NOT NULL, - vault TEXT NOT NULL, - - pool TEXT NOT NULL, - recent_root TEXT NOT NULL, - - transcript TEXT NOT NULL, - destination TEXT NOT NULL, - amount BIGINT NOT NULL CHECK (amount >= 0), - - intent TEXT NOT NULL, - action_id INTEGER NOT NULL, - - owner TEXT NOT NULL, - - state INTEGER NOT NULL, - - treasury_repaid BOOL NOT NULL, - repayment_diverted_to TEXT NULL, - - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_commitment__uniq__address UNIQUE (address), - CONSTRAINT codewallet__core_commitment__uniq__vault UNIQUE (vault), - CONSTRAINT codewallet__core_commitment__uniq__transcript UNIQUE (transcript), - CONSTRAINT codewallet__core_commitment__uniq__intent__and__action_id UNIQUE (intent, action_id) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_commitment; - ` -) - -var ( - testStore commitment.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestCommitmentPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/commitment/store.go b/pkg/code/data/commitment/store.go deleted file mode 100644 index 96cc1ea8..00000000 --- a/pkg/code/data/commitment/store.go +++ /dev/null @@ -1,49 +0,0 @@ -package commitment - -import ( - "context" - "errors" - - "github.com/code-payments/code-server/pkg/database/query" -) - -var ( - ErrCommitmentNotFound = errors.New("commitment not found") - ErrInvalidCommitment = errors.New("commitment record is invalid") -) - -type Store interface { - // Save saves a commitment account's state - Save(ctx context.Context, record *Record) error - - // GetByAddress gets a commitment account's state by its address - GetByAddress(ctx context.Context, address string) (*Record, error) - - // GetByVault gets a commitment account's state by its vault address - GetByVault(ctx context.Context, address string) (*Record, error) - - // GetByAction gets a commitment account's state by the action it's involved in - GetByAction(ctx context.Context, intentId string, actionId uint32) (*Record, error) - - // GetAllByState gets all commitment accounts in the provided state - GetAllByState(ctx context.Context, state State, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) - - // GetUpgradeableByOwner gets commitment records that are upgradeable and owned - // by a provided owner account. - GetUpgradeableByOwner(ctx context.Context, owner string, limit uint64) ([]*Record, error) - - // GetUsedTreasuryPoolDeficit gets the used deficit, in Kin quarks, to a treasury - // pool given the associated commitments - GetUsedTreasuryPoolDeficit(ctx context.Context, pool string) (uint64, error) - - // GetTotalTreasuryPoolDeficit gets the total deficit, in Kin quarks, to a treasury - // pool given the associated commitments - GetTotalTreasuryPoolDeficit(ctx context.Context, pool string) (uint64, error) - - // CountByState counts the number of commitment records in a given state - CountByState(ctx context.Context, state State) (uint64, error) - - // CountPendingRepaymentsDivertedToCommitment counts the number of commitments whose - // pending repayments are diverted to the provided one. - CountPendingRepaymentsDivertedToCommitment(ctx context.Context, address string) (uint64, error) -} diff --git a/pkg/code/data/commitment/tests/tests.go b/pkg/code/data/commitment/tests/tests.go deleted file mode 100644 index 47060b4d..00000000 --- a/pkg/code/data/commitment/tests/tests.go +++ /dev/null @@ -1,432 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "math" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/database/query" -) - -func RunTests(t *testing.T, s commitment.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s commitment.Store){ - testRoundTrip, - testUpdateConstraints, - testGetAllByState, - testGetUpgradeableByOwner, - testGetTreasuryPoolDeficit, - testCounts, - } { - tf(t, s) - teardown() - } -} - -func testRoundTrip(t *testing.T, s commitment.Store) { - t.Run("testRoundTrip", func(t *testing.T) { - ctx := context.Background() - - expected := &commitment.Record{ - Address: "address", - VaultAddress: "vault", - - Pool: "pool", - RecentRoot: "root", - - Transcript: "transcript", - Destination: "destination", - Amount: 12345, - - Intent: "intent", - ActionId: 1, - - Owner: "owner", - - State: commitment.StateOpen, - - CreatedAt: time.Now(), - } - - _, err := s.GetByAddress(ctx, expected.Address) - assert.Equal(t, commitment.ErrCommitmentNotFound, err) - - _, err = s.GetByVault(ctx, expected.VaultAddress) - assert.Equal(t, commitment.ErrCommitmentNotFound, err) - - _, err = s.GetByAction(ctx, expected.Intent, expected.ActionId) - assert.Equal(t, commitment.ErrCommitmentNotFound, err) - - cloned := expected.Clone() - require.NoError(t, s.Save(ctx, cloned)) - assert.EqualValues(t, 1, cloned.Id) - - actual, err := s.GetByAddress(ctx, expected.Address) - require.NoError(t, err) - assertEquivalentRecords(t, expected, actual) - - actual, err = s.GetByAction(ctx, expected.Intent, expected.ActionId) - require.NoError(t, err) - assertEquivalentRecords(t, expected, actual) - - otherCommitment := "other-commitment" - expected.TreasuryRepaid = true - expected.RepaymentDivertedTo = &otherCommitment - expected.State = commitment.StateClosed - cloned = expected.Clone() - require.NoError(t, s.Save(ctx, cloned)) - - actual, err = s.GetByAddress(ctx, expected.Address) - require.NoError(t, err) - assertEquivalentRecords(t, expected, actual) - - actual, err = s.GetByVault(ctx, expected.VaultAddress) - require.NoError(t, err) - assertEquivalentRecords(t, expected, actual) - - actual, err = s.GetByAction(ctx, expected.Intent, expected.ActionId) - require.NoError(t, err) - assertEquivalentRecords(t, expected, actual) - }) -} - -func testUpdateConstraints(t *testing.T, s commitment.Store) { - t.Run("testUpdateConstraints", func(t *testing.T) { - ctx := context.Background() - - otherCommitmentCommitment1 := "other-commitment-1" - otherCommitmentCommitment2 := "other-commitment-2" - expected := &commitment.Record{ - Address: "address", - VaultAddress: "vault", - - Pool: "pool", - RecentRoot: "root", - Transcript: "transcript", - - Destination: "destination", - Amount: 12345, - - Intent: "intent", - ActionId: 1, - - Owner: "owner", - - TreasuryRepaid: true, - RepaymentDivertedTo: &otherCommitmentCommitment1, - - State: commitment.StateClosed, - - CreatedAt: time.Now(), - } - require.NoError(t, s.Save(ctx, expected)) - - cloned := expected.Clone() - cloned.TreasuryRepaid = false - assert.Equal(t, commitment.ErrInvalidCommitment, s.Save(ctx, cloned)) - - cloned = expected.Clone() - cloned.State = commitment.StateOpen - assert.Equal(t, commitment.ErrInvalidCommitment, s.Save(ctx, cloned)) - - cloned = expected.Clone() - cloned.RepaymentDivertedTo = nil - assert.Equal(t, commitment.ErrInvalidCommitment, s.Save(ctx, cloned)) - - cloned = expected.Clone() - cloned.RepaymentDivertedTo = &otherCommitmentCommitment2 - assert.Equal(t, commitment.ErrInvalidCommitment, s.Save(ctx, cloned)) - - cloned = expected.Clone() - assert.NoError(t, s.Save(ctx, cloned)) - - actual, err := s.GetByAddress(ctx, expected.Address) - require.NoError(t, err) - assertEquivalentRecords(t, expected, actual) - }) -} - -func testGetAllByState(t *testing.T, s commitment.Store) { - t.Run("testGetAllByState", func(t *testing.T) { - ctx := context.Background() - - _, err := s.GetAllByState(ctx, commitment.StateOpen, query.EmptyCursor, 10, query.Ascending) - assert.Equal(t, commitment.ErrCommitmentNotFound, err) - - expected := []*commitment.Record{ - {Address: "commitment1", VaultAddress: "vault1", Pool: "pool", RecentRoot: "root", Transcript: "transcript1", Intent: "intent", ActionId: 0, Owner: "owner1", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {Address: "commitment2", VaultAddress: "vault2", Pool: "pool", RecentRoot: "root", Transcript: "transcript2", Intent: "intent", ActionId: 1, Owner: "owner2", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {Address: "commitment3", VaultAddress: "vault3", Pool: "pool", RecentRoot: "root", Transcript: "transcript3", Intent: "intent", ActionId: 2, Owner: "owner3", Destination: "destination", Amount: 123, State: commitment.StateOpen}, - {Address: "commitment4", VaultAddress: "vault4", Pool: "pool", RecentRoot: "root", Transcript: "transcript4", Intent: "intent", ActionId: 3, Owner: "owner4", Destination: "destination", Amount: 123, State: commitment.StateClosed}, - {Address: "commitment5", VaultAddress: "vault5", Pool: "pool", RecentRoot: "root", Transcript: "transcript5", Intent: "intent", ActionId: 4, Owner: "owner5", Destination: "destination", Amount: 123, State: commitment.StateClosed}, - } - for _, record := range expected { - require.NoError(t, s.Save(ctx, record)) - } - - _, err = s.GetAllByState(ctx, commitment.StateUnknown, query.EmptyCursor, 10, query.Ascending) - assert.Equal(t, commitment.ErrCommitmentNotFound, err) - - actual, err := s.GetAllByState(ctx, commitment.StateOpen, query.EmptyCursor, 10, query.Ascending) - require.NoError(t, err) - assert.Len(t, actual, 3) - - actual, err = s.GetAllByState(ctx, commitment.StateClosed, query.EmptyCursor, 10, query.Ascending) - require.NoError(t, err) - assert.Len(t, actual, 2) - - // Check items (asc) - actual, err = s.GetAllByState(ctx, commitment.StateOpen, query.EmptyCursor, 5, query.Ascending) - require.NoError(t, err) - require.Len(t, actual, 3) - assert.Equal(t, "commitment1", actual[0].Address) - assert.Equal(t, "commitment2", actual[1].Address) - assert.Equal(t, "commitment3", actual[2].Address) - - // Check items (desc) - actual, err = s.GetAllByState(ctx, commitment.StateOpen, query.EmptyCursor, 5, query.Descending) - require.NoError(t, err) - require.Len(t, actual, 3) - assert.Equal(t, "commitment3", actual[0].Address) - assert.Equal(t, "commitment2", actual[1].Address) - assert.Equal(t, "commitment1", actual[2].Address) - - // Check items (asc + limit) - actual, err = s.GetAllByState(ctx, commitment.StateOpen, query.EmptyCursor, 2, query.Ascending) - require.NoError(t, err) - require.Len(t, actual, 2) - assert.Equal(t, "commitment1", actual[0].Address) - assert.Equal(t, "commitment2", actual[1].Address) - - // Check items (desc + limit) - actual, err = s.GetAllByState(ctx, commitment.StateOpen, query.EmptyCursor, 2, query.Descending) - require.NoError(t, err) - require.Len(t, actual, 2) - assert.Equal(t, "commitment3", actual[0].Address) - assert.Equal(t, "commitment2", actual[1].Address) - - // Check items (asc + cursor) - actual, err = s.GetAllByState(ctx, commitment.StateOpen, query.ToCursor(1), 5, query.Ascending) - require.NoError(t, err) - require.Len(t, actual, 2) - assert.Equal(t, "commitment2", actual[0].Address) - assert.Equal(t, "commitment3", actual[1].Address) - - // Check items (desc + cursor) - actual, err = s.GetAllByState(ctx, commitment.StateOpen, query.ToCursor(3), 5, query.Descending) - require.NoError(t, err) - require.Len(t, actual, 2) - assert.Equal(t, "commitment2", actual[0].Address) - assert.Equal(t, "commitment1", actual[1].Address) - }) -} - -func testGetUpgradeableByOwner(t *testing.T, s commitment.Store) { - t.Run("testGetUpgradeableByOwner", func(t *testing.T) { - ctx := context.Background() - - _, err := s.GetUpgradeableByOwner(ctx, "owner", 10) - assert.Equal(t, commitment.ErrCommitmentNotFound, err) - - futureCommitment := "future-commitment" - records := []*commitment.Record{ - {State: commitment.StateUnknown, Owner: "owner1"}, - {State: commitment.StatePayingDestination, Owner: "owner1"}, - {State: commitment.StateReadyToOpen, Owner: "owner1"}, - {State: commitment.StateReadyToOpen, Owner: "owner1", RepaymentDivertedTo: &futureCommitment}, - {State: commitment.StateOpening, Owner: "owner2"}, - {State: commitment.StateOpening, Owner: "owner2", RepaymentDivertedTo: &futureCommitment}, - {State: commitment.StateOpen, Owner: "owner2"}, - {State: commitment.StateOpen, Owner: "owner2", RepaymentDivertedTo: &futureCommitment}, - {State: commitment.StateClosing, Owner: "owner2"}, - {State: commitment.StateClosing, Owner: "owner2", RepaymentDivertedTo: &futureCommitment}, - {State: commitment.StateClosed, Owner: "owner2"}, - {State: commitment.StateClosed, Owner: "owner2", RepaymentDivertedTo: &futureCommitment}, - {State: commitment.StateReadyToRemoveFromMerkleTree, Owner: "owner1"}, - {State: commitment.StateReadyToRemoveFromMerkleTree, Owner: "owner1", RepaymentDivertedTo: &futureCommitment}, - {State: commitment.StateRemovedFromMerkleTree, Owner: "owner1"}, - {State: commitment.StateRemovedFromMerkleTree, Owner: "owner1", RepaymentDivertedTo: &futureCommitment}, - } - - for i, record := range records { - // Populate data irrelevant to test - record.Pool = "pool" - record.RecentRoot = "root" - record.Address = fmt.Sprintf("address%d", i) - record.VaultAddress = fmt.Sprintf("vault%d", i) - record.RecentRoot = fmt.Sprintf("root%d", i) - record.Transcript = fmt.Sprintf("transcript%d", i) - record.Destination = fmt.Sprintf("destination%d", i) - record.Amount = 100 - record.Intent = fmt.Sprintf("intent%d", i) - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, record)) - } - - _, err = s.GetUpgradeableByOwner(ctx, "owner", 10) - assert.Equal(t, commitment.ErrCommitmentNotFound, err) - - actual, err := s.GetUpgradeableByOwner(ctx, "owner1", 10) - require.Nil(t, err) - require.Len(t, actual, 1) - assert.Equal(t, records[2].Address, actual[0].Address) - - actual, err = s.GetUpgradeableByOwner(ctx, "owner2", 10) - require.Nil(t, err) - require.Len(t, actual, 4) - assert.Equal(t, records[4].Address, actual[0].Address) - assert.Equal(t, records[6].Address, actual[1].Address) - assert.Equal(t, records[8].Address, actual[2].Address) - assert.Equal(t, records[10].Address, actual[3].Address) - - actual, err = s.GetUpgradeableByOwner(ctx, "owner2", 2) - require.Nil(t, err) - require.Len(t, actual, 2) - assert.Equal(t, records[4].Address, actual[0].Address) - assert.Equal(t, records[6].Address, actual[1].Address) - }) -} - -func testGetTreasuryPoolDeficit(t *testing.T, s commitment.Store) { - t.Run("testGetTreasuryPoolDeficit", func(t *testing.T) { - ctx := context.Background() - - records := []*commitment.Record{ - {State: commitment.StateUnknown}, - {State: commitment.StatePayingDestination}, - {State: commitment.StateReadyToOpen}, - {State: commitment.StateOpening}, - {State: commitment.StateOpen}, - {State: commitment.StateClosing}, - {State: commitment.StateClosed}, - {State: commitment.StateReadyToRemoveFromMerkleTree}, - {State: commitment.StateRemovedFromMerkleTree}, - } - - for i, record := range records { - record.Pool = "pool" - record.Amount = uint64(math.Pow10(i)) - - // Populate data irrelevant to test - record.Address = fmt.Sprintf("address%d", i) - record.VaultAddress = fmt.Sprintf("vault%d", i) - record.RecentRoot = fmt.Sprintf("root%d", i) - record.Transcript = fmt.Sprintf("transcript%d", i) - record.Destination = fmt.Sprintf("destination%d", i) - record.Intent = fmt.Sprintf("intent%d", i) - record.Owner = fmt.Sprintf("owner%d", i) - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, record)) - } - - actual, err := s.GetUsedTreasuryPoolDeficit(ctx, "pool") - require.NoError(t, err) - assert.EqualValues(t, 111111110, actual) - - actual, err = s.GetTotalTreasuryPoolDeficit(ctx, "pool") - require.NoError(t, err) - assert.EqualValues(t, 111111111, actual) - - actual, err = s.GetUsedTreasuryPoolDeficit(ctx, "other") - require.NoError(t, err) - assert.EqualValues(t, 0, actual) - - for i, record := range records { - if i%2 == 0 { - record.TreasuryRepaid = true - require.NoError(t, s.Save(ctx, record)) - } - } - - actual, err = s.GetUsedTreasuryPoolDeficit(ctx, "pool") - require.NoError(t, err) - assert.EqualValues(t, 10101010, actual) - - actual, err = s.GetTotalTreasuryPoolDeficit(ctx, "pool") - require.NoError(t, err) - assert.EqualValues(t, 10101010, actual) - }) -} - -func testCounts(t *testing.T, s commitment.Store) { - t.Run("testCounts", func(t *testing.T) { - ctx := context.Background() - - futureCommitment1 := "future-commitment-1" - futureCommitment2 := "future-commitment-2" - futureCommitment3 := "future-commitment-3" - records := []*commitment.Record{ - {State: commitment.StateReadyToOpen, RecentRoot: "root1", RepaymentDivertedTo: &futureCommitment1}, - {State: commitment.StateReadyToOpen, RecentRoot: "root1", RepaymentDivertedTo: &futureCommitment1}, - {State: commitment.StateReadyToOpen, RecentRoot: "root2", RepaymentDivertedTo: &futureCommitment2}, - {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureCommitment2, TreasuryRepaid: true}, - {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureCommitment2}, - {State: commitment.StateClosed, RecentRoot: "root3", RepaymentDivertedTo: &futureCommitment2}, - } - - for i, record := range records { - // Populate data irrelevant to test - record.Pool = "pool" - record.Amount = 1 - record.Address = fmt.Sprintf("address%d", i) - record.VaultAddress = fmt.Sprintf("vault%d", i) - record.Transcript = fmt.Sprintf("transcript%d", i) - record.Destination = fmt.Sprintf("destination%d", i) - record.Intent = fmt.Sprintf("intent%d", i) - record.Owner = fmt.Sprintf("owner%d", i) - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, record)) - } - - count, err := s.CountByState(ctx, commitment.StateOpen) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - count, err = s.CountByState(ctx, commitment.StateReadyToOpen) - require.NoError(t, err) - assert.EqualValues(t, 3, count) - - count, err = s.CountByState(ctx, commitment.StateClosed) - require.NoError(t, err) - assert.EqualValues(t, 3, count) - - count, err = s.CountPendingRepaymentsDivertedToCommitment(ctx, futureCommitment1) - require.NoError(t, err) - assert.EqualValues(t, 2, count) - - count, err = s.CountPendingRepaymentsDivertedToCommitment(ctx, futureCommitment2) - require.NoError(t, err) - assert.EqualValues(t, 3, count) - - count, err = s.CountPendingRepaymentsDivertedToCommitment(ctx, futureCommitment3) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - }) -} - -func assertEquivalentRecords(t *testing.T, obj1, obj2 *commitment.Record) { - assert.Equal(t, obj1.Address, obj2.Address) - assert.Equal(t, obj1.VaultAddress, obj2.VaultAddress) - assert.Equal(t, obj1.Pool, obj2.Pool) - assert.Equal(t, obj1.RecentRoot, obj2.RecentRoot) - assert.Equal(t, obj1.Transcript, obj2.Transcript) - assert.Equal(t, obj1.Destination, obj2.Destination) - assert.Equal(t, obj1.Amount, obj2.Amount) - assert.Equal(t, obj1.Intent, obj2.Intent) - assert.Equal(t, obj1.ActionId, obj2.ActionId) - assert.Equal(t, obj1.Owner, obj2.Owner) - assert.Equal(t, obj1.TreasuryRepaid, obj2.TreasuryRepaid) - assert.EqualValues(t, obj1.RepaymentDivertedTo, obj2.RepaymentDivertedTo) - assert.Equal(t, obj1.State, obj2.State) -} diff --git a/pkg/code/data/contact/memory/store.go b/pkg/code/data/contact/memory/store.go deleted file mode 100644 index 5a09c638..00000000 --- a/pkg/code/data/contact/memory/store.go +++ /dev/null @@ -1,121 +0,0 @@ -package memory - -import ( - "context" - "encoding/binary" - "errors" - "sync" - - "github.com/code-payments/code-server/pkg/code/data/contact" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -type store struct { - mu sync.RWMutex - contactsByOwner map[string][]string -} - -// New returns a new postgres backed contact.Store -func New() contact.Store { - return &store{ - contactsByOwner: make(map[string][]string), - } -} - -// Add implements contact.Store.Add -func (s *store) Add(ctx context.Context, owner *user.DataContainerID, contact string) error { - s.mu.Lock() - defer s.mu.Unlock() - - contacts := s.contactsByOwner[owner.String()] - for _, existing := range contacts { - if existing == contact { - return nil - } - } - - contacts = append(contacts, contact) - s.contactsByOwner[owner.String()] = contacts - - return nil -} - -// BatchAdd implements contact.Store.BatchAdd -func (s *store) BatchAdd(ctx context.Context, owner *user.DataContainerID, contacts []string) error { - for _, contact := range contacts { - err := s.Add(ctx, owner, contact) - if err != nil { - return err - } - } - return nil -} - -// Remove implements contact.Store.Remove -func (s *store) Remove(ctx context.Context, owner *user.DataContainerID, contact string) error { - s.mu.Lock() - defer s.mu.Unlock() - - contacts := s.contactsByOwner[owner.String()] - for i, existing := range contacts { - if existing == contact { - s.contactsByOwner[owner.String()] = append(contacts[:i], contacts[i+1:]...) - return nil - } - } - - return nil -} - -// BatchRemove implements contact.Store.BatchRemove -func (s *store) BatchRemove(ctx context.Context, owner *user.DataContainerID, contacts []string) error { - for _, contact := range contacts { - err := s.Remove(ctx, owner, contact) - if err != nil { - return err - } - } - return nil -} - -// Get implements contact.Store.Get -func (s *store) Get(ctx context.Context, owner *user.DataContainerID, limit uint32, pageToken []byte) ([]string, []byte, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - var lowerBound uint64 - if len(pageToken) == 8 { - lowerBound = binary.LittleEndian.Uint64(pageToken) - } else if len(pageToken) != 0 { - return nil, nil, errors.New("invalid page token bytes") - } - - contacts, ok := s.contactsByOwner[owner.String()] - if !ok { - return nil, nil, nil - } - - if len(contacts) <= int(lowerBound) { - return nil, nil, nil - } - - upperBound := lowerBound + uint64(limit) - if int(upperBound) > len(contacts) { - upperBound = uint64(len(contacts)) - } - - var nextPageToken []byte - if len(contacts) > int(upperBound) { - nextPageToken = make([]byte, 8) - binary.LittleEndian.PutUint64(nextPageToken, upperBound) - } - - return contacts[lowerBound:upperBound], nextPageToken, nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.contactsByOwner = make(map[string][]string) -} diff --git a/pkg/code/data/contact/memory/store_test.go b/pkg/code/data/contact/memory/store_test.go deleted file mode 100644 index 1861de30..00000000 --- a/pkg/code/data/contact/memory/store_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/contact/tests" -) - -func TestContactMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/contact/postgres/model.go b/pkg/code/data/contact/postgres/model.go deleted file mode 100644 index f5dc17fe..00000000 --- a/pkg/code/data/contact/postgres/model.go +++ /dev/null @@ -1,156 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/phone" - "github.com/code-payments/code-server/pkg/code/data/user" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" -) - -const ( - tableName = "codewallet__core_contactlist" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - OwnerID string `db:"owner_id"` - Contact string `db:"contact"` -} - -func newModel(owner *user.DataContainerID, contact string) (*model, error) { - if err := owner.Validate(); err != nil { - return nil, err - } - - if !phone.IsE164Format(contact) { - return nil, errors.New("contact phone number doesn't match E.164 standard") - } - - return &model{ - OwnerID: owner.String(), - // todo: Drop this eventually. - Contact: contact, - }, nil -} - -func newModels(owner *user.DataContainerID, contacts []string) ([]*model, error) { - models := make([]*model, len(contacts)) - for i, contact := range contacts { - model, err := newModel(owner, contact) - if err != nil { - return nil, err - } - - models[i] = model - } - - return models, nil -} - -func (m *model) dbAdd(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + tableName + ` - (owner_id, contact) - VALUES ($1, $2) - ON CONFLICT DO NOTHING - RETURNING id, owner_id, contact` - - err := db.QueryRowxContext(ctx, query, m.OwnerID, m.Contact).StructScan(m) - return pgutil.CheckNoRows(err, nil) -} - -func (m *model) dbRemove(ctx context.Context, db *sqlx.DB) error { - query := `DELETE FROM ` + tableName + ` - WHERE owner_id = $1 and contact = $2 - RETURNING id, owner_id, contact` - - err := db.QueryRowxContext(ctx, query, m.OwnerID, m.Contact).StructScan(m) - return pgutil.CheckNoRows(err, nil) -} - -func dbBatchAdd(ctx context.Context, db *sqlx.DB, owner *user.DataContainerID, contacts []string) error { - if len(contacts) == 0 { - return nil - } - - // Mostly doing this to reuse basic validation logic - models, err := newModels(owner, contacts) - if err != nil { - return err - } - - // todo: is there a better way to construct the query? - query := fmt.Sprintf("INSERT INTO %s (owner_id, contact) VALUES \n", tableName) - - var entries []string - for _, model := range models { - entries = append(entries, fmt.Sprintf("('%s', '%s')", model.OwnerID, model.Contact)) - } - - query += strings.Join(entries, ",") - query += "\nON CONFLICT (owner_id, contact) DO NOTHING" - - _, err = db.ExecContext(ctx, query) - return err -} - -func dbBatchRemove(ctx context.Context, db *sqlx.DB, owner *user.DataContainerID, contacts []string) error { - if len(contacts) == 0 { - return nil - } - - // Mostly doing this to reuse basic validation logic - models, err := newModels(owner, contacts) - if err != nil { - return err - } - - contactArgs := make([]string, len(models)) - for i, model := range models { - contactArgs[i] = fmt.Sprintf("'%s'", model.Contact) - } - - // todo: is there a better way to construct the query? - query := fmt.Sprintf( - "DELETE FROM %s WHERE owner_id = '%s' AND contact IN (%s)\n", - tableName, - models[0].OwnerID, - strings.Join(contactArgs, ","), - ) - - _, err = db.ExecContext(ctx, query) - return err -} - -func dbGetByOwner(ctx context.Context, db *sqlx.DB, owner *user.DataContainerID, exclusiveLowerBoundID uint64, limit uint32) ([]*model, bool, error) { - var res []*model - var isLastPage bool - - query := ` - SELECT id, owner_id, contact FROM ` + tableName + ` - WHERE owner_id = $1 AND id > $2 - ORDER BY id ASC - LIMIT $3 - ` - - err := db.SelectContext(ctx, &res, query, owner.String(), exclusiveLowerBoundID, limit+1) - if err != nil { - return nil, false, err - } - - if len(res) > int(limit) { - res = res[:limit] - isLastPage = false - } else { - isLastPage = true - } - - return res, isLastPage, nil -} diff --git a/pkg/code/data/contact/postgres/store.go b/pkg/code/data/contact/postgres/store.go deleted file mode 100644 index 0455f944..00000000 --- a/pkg/code/data/contact/postgres/store.go +++ /dev/null @@ -1,82 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "encoding/binary" - "errors" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/contact" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres backed contact.Store -func New(db *sql.DB) contact.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Add implements contact.Store.Add -func (s *store) Add(ctx context.Context, owner *user.DataContainerID, contact string) error { - model, err := newModel(owner, contact) - if err != nil { - return err - } - - return model.dbAdd(ctx, s.db) -} - -// BatchAdd implements contact.Store.BatchAdd -func (s *store) BatchAdd(ctx context.Context, owner *user.DataContainerID, contacts []string) error { - return dbBatchAdd(ctx, s.db, owner, contacts) -} - -// Remove implements contact.Store.Remove -func (s *store) Remove(ctx context.Context, owner *user.DataContainerID, contact string) error { - model, err := newModel(owner, contact) - if err != nil { - return err - } - - return model.dbRemove(ctx, s.db) -} - -// BatchRemove implements contact.Store.BatchRemove -func (s *store) BatchRemove(ctx context.Context, owner *user.DataContainerID, contacts []string) error { - return dbBatchRemove(ctx, s.db, owner, contacts) -} - -// Get implements contact.Store.Get -func (s *store) Get(ctx context.Context, owner *user.DataContainerID, limit uint32, pageToken []byte) ([]string, []byte, error) { - var exclusiveLowerBoundID uint64 - if len(pageToken) == 8 { - exclusiveLowerBoundID = binary.LittleEndian.Uint64(pageToken) - } else if len(pageToken) != 0 { - return nil, nil, errors.New("invalid page token bytes") - } - - models, isLastPage, err := dbGetByOwner(ctx, s.db, owner, exclusiveLowerBoundID, limit) - if err != nil { - return nil, nil, err - } - - var nextPageToken []byte - if !isLastPage { - nextPageToken = make([]byte, 8) - binary.LittleEndian.PutUint64(nextPageToken, uint64(models[len(models)-1].Id.Int64)) - } - - contacts := make([]string, len(models)) - for i, model := range models { - contacts[i] = model.Contact - } - - return contacts, nextPageToken, nil -} diff --git a/pkg/code/data/contact/postgres/store_test.go b/pkg/code/data/contact/postgres/store_test.go deleted file mode 100644 index 5e87e524..00000000 --- a/pkg/code/data/contact/postgres/store_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/contact" - "github.com/code-payments/code-server/pkg/code/data/contact/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -var ( - testStore contact.Store - teardown func() -) - -type Schema struct { - create string - drop string -} - -var defaultSchema = Schema{ - // Used for testing ONLY, the table and migrations are external to this repository - create: ` - CREATE TABLE codewallet__core_contactlist( - id SERIAL NOT NULL PRIMARY KEY, - - owner_id UUID NOT NULL, - contact TEXT NOT NULL, - - CONSTRAINT codewallet__core_contactlist__uniq__owner_id__and__contact UNIQUE (owner_id, contact) - ); - `, - // Used for testing ONLY, the table and migrations are external to this repository - drop: ` - DROP TABLE codewallet__core_contactlist; - `, -} - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestContactPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(defaultSchema.create) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(defaultSchema.drop) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/contact/store.go b/pkg/code/data/contact/store.go deleted file mode 100644 index 8b2e07a4..00000000 --- a/pkg/code/data/contact/store.go +++ /dev/null @@ -1,28 +0,0 @@ -package contact - -import ( - "context" - - "github.com/code-payments/code-server/pkg/code/data/user" -) - -type Store interface { - // Add adds the contact to the owner's contact list. This call is idempotent - // and will not fail on duplicate insertion. - Add(ctx context.Context, owner *user.DataContainerID, contact string) error - - // BatchAdd adds a batch of contacts to the owner's contact list. This call is - // idempotent and will not fail on duplicate insertion. - BatchAdd(ctx context.Context, owner *user.DataContainerID, contacts []string) error - - // Remove removes the contact to the owner's contact list. This call is - // idempotent and will not fail on deletion of a non-existant entry. - Remove(ctx context.Context, owner *user.DataContainerID, contact string) error - - // BatchDelete removes a batch of contacts to the owner's contact list. This - // call is idempotent and will not fail on duplicate insertion. - BatchRemove(ctx context.Context, owner *user.DataContainerID, contacts []string) error - - // Get gets a page of contacts from an owner's contact list. - Get(ctx context.Context, owner *user.DataContainerID, limit uint32, pageToken []byte) (contacts []string, nextPageToken []byte, err error) -} diff --git a/pkg/code/data/contact/tests/tests.go b/pkg/code/data/contact/tests/tests.go deleted file mode 100644 index a2b7c860..00000000 --- a/pkg/code/data/contact/tests/tests.go +++ /dev/null @@ -1,128 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/contact" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -func RunTests(t *testing.T, s contact.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s contact.Store){ - testHappyPath, - testHappyPathForBatchCalls, - testContractRetrievalPaging, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s contact.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - owner := user.NewDataContainerID() - contact := "+11234567890" - - actual, _, err := s.Get(ctx, owner, 100, nil) - require.NoError(t, err) - assert.Empty(t, actual) - - require.NoError(t, s.Add(ctx, owner, contact)) - require.NoError(t, s.Add(ctx, owner, contact)) - - actual, _, err = s.Get(ctx, owner, 100, nil) - require.NoError(t, err) - require.Len(t, actual, 1) - assert.Equal(t, actual[0], contact) - - require.NoError(t, s.Remove(ctx, owner, contact)) - require.NoError(t, s.Remove(ctx, owner, contact)) - - actual, _, err = s.Get(ctx, owner, 100, nil) - require.NoError(t, err) - assert.Empty(t, actual) - }) -} - -func testHappyPathForBatchCalls(t *testing.T, s contact.Store) { - t.Run("testHappyPathForBatchCalls", func(t *testing.T) { - ctx := context.Background() - - owner := user.NewDataContainerID() - contacts := make([]string, 0) - for i := 0; i < 10; i++ { - contacts = append(contacts, fmt.Sprintf("+1800555000%d", i)) - } - - actual, _, err := s.Get(ctx, owner, 100, nil) - require.NoError(t, err) - assert.Empty(t, actual) - - require.NoError(t, s.BatchAdd(ctx, owner, contacts[:len(contacts)/2])) - require.NoError(t, s.BatchAdd(ctx, owner, contacts)) - - actual, _, err = s.Get(ctx, owner, 100, nil) - require.NoError(t, err) - require.Len(t, actual, len(contacts)) - assert.Equal(t, contacts, actual) - - require.NoError(t, s.BatchRemove(ctx, owner, contacts[:len(contacts)/2])) - require.NoError(t, s.BatchRemove(ctx, owner, contacts)) - - actual, _, err = s.Get(ctx, owner, 10, nil) - require.NoError(t, err) - assert.Empty(t, actual) - }) -} - -func testContractRetrievalPaging(t *testing.T, s contact.Store) { - t.Run("testContractRetrievalPaging", func(t *testing.T) { - ctx := context.Background() - - owner := user.NewDataContainerID() - - contacts := make([]string, 0) - for i := 0; i < 10; i++ { - contacts = append(contacts, fmt.Sprintf("+1800555000%d", i)) - require.NoError(t, s.Add(ctx, owner, contacts[i])) - } - - for pageSize := 1; pageSize <= len(contacts); pageSize++ { - var currentPageToken []byte - var callCount int - - var actual []string - for { - callCount++ - - if callCount > len(contacts)/pageSize+1 { - assert.Fail(t, "exceeded maximum call count") - } - - subset, nextPageToken, err := s.Get(ctx, owner, uint32(pageSize), currentPageToken) - require.NoError(t, err) - - actual = append(actual, subset...) - - if len(nextPageToken) == 0 { - assert.True(t, len(subset) <= pageSize) - break - } - - assert.Len(t, subset, pageSize) - - currentPageToken = nextPageToken - } - - require.Len(t, actual, len(contacts)) - assert.Equal(t, contacts, actual) - } - }) -} diff --git a/pkg/code/data/event/memory/store.go b/pkg/code/data/event/memory/store.go deleted file mode 100644 index 0970b229..00000000 --- a/pkg/code/data/event/memory/store.go +++ /dev/null @@ -1,102 +0,0 @@ -package memory - -import ( - "context" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/event" -) - -type store struct { - mu sync.Mutex - last uint64 - records []*event.Record -} - -// New returns a new in memory event.Store -func New() event.Store { - return &store{} -} - -// Save implements event.Store.Save -func (s *store) Save(_ context.Context, data *event.Record) error { - if err := data.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - if item := s.find(data); item != nil { - item.DestinationCodeAccount = pointer.StringCopy(data.DestinationCodeAccount) - item.DestinationIdentity = pointer.StringCopy(data.DestinationIdentity) - - item.DestinationClientIp = pointer.StringCopy(data.DestinationClientIp) - item.DestinationClientCity = pointer.StringCopy(data.DestinationClientCity) - item.DestinationClientCountry = pointer.StringCopy(data.DestinationClientCountry) - - item.SpamConfidence = data.SpamConfidence - - item.CopyTo(data) - } else { - if data.Id == 0 { - data.Id = s.last - } - if data.CreatedAt.IsZero() { - data.CreatedAt = time.Now() - } - - cloned := data.Clone() - s.records = append(s.records, &cloned) - } - - return nil -} - -// Get implements event.Store.Get -func (s *store) Get(_ context.Context, id string) (*event.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findByEventId(id) - if item == nil { - return nil, event.ErrEventNotFound - } - - cloned := item.Clone() - return &cloned, nil -} - -func (s *store) find(data *event.Record) *event.Record { - for _, item := range s.records { - if item.Id == data.Id { - return item - } - - if item.EventId == data.EventId { - return item - } - } - - return nil -} - -func (s *store) findByEventId(id string) *event.Record { - for _, item := range s.records { - if item.EventId == id { - return item - } - } - - return nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - s.last = 0 - s.records = nil -} diff --git a/pkg/code/data/event/memory/store_test.go b/pkg/code/data/event/memory/store_test.go deleted file mode 100644 index 86a836a3..00000000 --- a/pkg/code/data/event/memory/store_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/event/tests" -) - -func TestEventMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/event/postgres/model.go b/pkg/code/data/event/postgres/model.go deleted file mode 100644 index 080b61fd..00000000 --- a/pkg/code/data/event/postgres/model.go +++ /dev/null @@ -1,193 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/event" -) - -const ( - tableName = "codewallet__core_event" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - - EventId string `db:"event_id"` - EventType uint32 `db:"event_type"` - - SourceCodeAccount string `db:"source_code_account"` - DestinationCodeAccount sql.NullString `db:"destination_code_account"` - ExternalTokenAccount sql.NullString `db:"external_token_account"` - - SourceIdentity string `db:"source_identity"` - DestinationIdentity sql.NullString `db:"destination_identity"` - - SourceClientIp sql.NullString `db:"source_client_ip"` - SourceClientCity sql.NullString `db:"source_client_city"` - SourceClientCountry sql.NullString `db:"source_client_country"` - DestinationClientIp sql.NullString `db:"destination_client_ip"` - DestinationClientCity sql.NullString `db:"destination_client_city"` - DestinationClientCountry sql.NullString `db:"destination_client_country"` - - UsdValue sql.NullFloat64 `db:"usd_value"` - - SpamConfidence float64 `db:"spam_confidence"` - - CreatedAt time.Time `db:"created_at"` -} - -func toModel(obj *event.Record) (*model, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &model{ - EventId: obj.EventId, - EventType: uint32(obj.EventType), - - SourceCodeAccount: obj.SourceCodeAccount, - DestinationCodeAccount: sql.NullString{ - Valid: obj.DestinationCodeAccount != nil, - String: *pointer.StringOrDefault(obj.DestinationCodeAccount, ""), - }, - ExternalTokenAccount: sql.NullString{ - Valid: obj.ExternalTokenAccount != nil, - String: *pointer.StringOrDefault(obj.ExternalTokenAccount, ""), - }, - - SourceIdentity: obj.SourceIdentity, - DestinationIdentity: sql.NullString{ - Valid: obj.DestinationIdentity != nil, - String: *pointer.StringOrDefault(obj.DestinationIdentity, ""), - }, - - SourceClientIp: sql.NullString{ - Valid: obj.SourceClientIp != nil, - String: *pointer.StringOrDefault(obj.SourceClientIp, ""), - }, - SourceClientCity: sql.NullString{ - Valid: obj.SourceClientCity != nil, - String: *pointer.StringOrDefault(obj.SourceClientCity, ""), - }, - SourceClientCountry: sql.NullString{ - Valid: obj.SourceClientCountry != nil, - String: *pointer.StringOrDefault(obj.SourceClientCountry, ""), - }, - DestinationClientIp: sql.NullString{ - Valid: obj.DestinationClientIp != nil, - String: *pointer.StringOrDefault(obj.DestinationClientIp, ""), - }, - DestinationClientCity: sql.NullString{ - Valid: obj.DestinationClientCity != nil, - String: *pointer.StringOrDefault(obj.DestinationClientCity, ""), - }, - DestinationClientCountry: sql.NullString{ - Valid: obj.DestinationClientCountry != nil, - String: *pointer.StringOrDefault(obj.DestinationClientCountry, ""), - }, - - UsdValue: sql.NullFloat64{ - Valid: obj.UsdValue != nil, - Float64: *pointer.Float64OrDefault(obj.UsdValue, 0), - }, - - SpamConfidence: obj.SpamConfidence, - - CreatedAt: obj.CreatedAt, - }, nil -} - -func fromModel(obj *model) *event.Record { - return &event.Record{ - Id: uint64(obj.Id.Int64), - - EventId: obj.EventId, - EventType: event.EventType(obj.EventType), - - SourceCodeAccount: obj.SourceCodeAccount, - DestinationCodeAccount: pointer.StringIfValid(obj.DestinationCodeAccount.Valid, obj.DestinationCodeAccount.String), - ExternalTokenAccount: pointer.StringIfValid(obj.ExternalTokenAccount.Valid, obj.ExternalTokenAccount.String), - - SourceIdentity: obj.SourceIdentity, - DestinationIdentity: pointer.StringIfValid(obj.DestinationIdentity.Valid, obj.DestinationIdentity.String), - - SourceClientIp: pointer.StringIfValid(obj.SourceClientIp.Valid, obj.SourceClientIp.String), - SourceClientCity: pointer.StringIfValid(obj.SourceClientCity.Valid, obj.SourceClientCity.String), - SourceClientCountry: pointer.StringIfValid(obj.SourceClientCountry.Valid, obj.SourceClientCountry.String), - DestinationClientIp: pointer.StringIfValid(obj.DestinationClientIp.Valid, obj.DestinationClientIp.String), - DestinationClientCity: pointer.StringIfValid(obj.DestinationClientCity.Valid, obj.DestinationClientCity.String), - DestinationClientCountry: pointer.StringIfValid(obj.DestinationClientCountry.Valid, obj.DestinationClientCountry.String), - - UsdValue: pointer.Float64IfValid(obj.UsdValue.Valid, obj.UsdValue.Float64), - - SpamConfidence: obj.SpamConfidence, - - CreatedAt: obj.CreatedAt, - } -} - -func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `INSERT INTO ` + tableName + ` - (event_id, event_type, source_code_account, destination_code_account, external_token_account, source_identity, destination_identity, source_client_ip, source_client_city, source_client_country, destination_client_ip, destination_client_city, destination_client_country, usd_value, spam_confidence, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) - ON CONFLICT (event_id) - DO UPDATE - SET destination_code_account = $4, destination_identity = $7, destination_client_ip = $11, destination_client_city = $12, destination_client_country = $13, spam_confidence = $15 - WHERE ` + tableName + `.event_id = $1 - RETURNING id, event_id, event_type, source_code_account, destination_code_account, external_token_account, source_identity, destination_identity, source_client_ip, source_client_city, source_client_country, destination_client_ip, destination_client_city, destination_client_country, usd_value, spam_confidence, created_at` - - if m.CreatedAt.IsZero() { - m.CreatedAt = time.Now() - } - - return db.QueryRowxContext( - ctx, - query, - - m.EventId, - m.EventType, - - m.SourceCodeAccount, - m.DestinationCodeAccount, - m.ExternalTokenAccount, - - m.SourceIdentity, - m.DestinationIdentity, - - m.SourceClientIp, - m.SourceClientCity, - m.SourceClientCountry, - m.DestinationClientIp, - m.DestinationClientCity, - m.DestinationClientCountry, - - m.UsdValue, - - m.SpamConfidence, - - m.CreatedAt, - ).StructScan(m) - }) -} - -func dbGet(ctx context.Context, db *sqlx.DB, id string) (*model, error) { - var res model - - query := `SELECT id, event_id, event_type, source_code_account, destination_code_account, external_token_account, source_identity, destination_identity, source_client_ip, source_client_city, source_client_country, destination_client_ip, destination_client_city, destination_client_country, usd_value, spam_confidence, created_at FROM ` + tableName + ` - WHERE event_id = $1 - ` - - err := db.GetContext(ctx, &res, query, id) - if err != nil { - return nil, pgutil.CheckNoRows(err, event.ErrEventNotFound) - } - return &res, nil -} diff --git a/pkg/code/data/event/postgres/store.go b/pkg/code/data/event/postgres/store.go deleted file mode 100644 index 715bc187..00000000 --- a/pkg/code/data/event/postgres/store.go +++ /dev/null @@ -1,46 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/event" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres-backed rendezvous.Store -func New(db *sql.DB) event.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Save implements event.Store.Save -func (s *store) Save(ctx context.Context, record *event.Record) error { - model, err := toModel(record) - if err != nil { - return err - } - - err = model.dbSave(ctx, s.db) - if err != nil { - return err - } - - fromModel(model).CopyTo(record) - return nil -} - -// Get implements event.Store.Get -func (s *store) Get(ctx context.Context, id string) (*event.Record, error) { - model, err := dbGet(ctx, s.db, id) - if err != nil { - return nil, err - } - return fromModel(model), nil -} diff --git a/pkg/code/data/event/postgres/store_test.go b/pkg/code/data/event/postgres/store_test.go deleted file mode 100644 index ed9ed2ea..00000000 --- a/pkg/code/data/event/postgres/store_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/event" - "github.com/code-payments/code-server/pkg/code/data/event/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_event( - id SERIAL NOT NULL PRIMARY KEY, - - event_id TEXT NOT NULL UNIQUE, - event_type INTEGER NOT NULL, - - source_code_account TEXT NOT NULL, - destination_code_account TEXT NULL, - external_token_account TEXT NULL, - - source_identity TEXT NOT NULL, - destination_identity TEXT NULL, - - source_client_ip TEXT NOT NULL, - source_client_city TEXT NULL, - source_client_country TEXT NULL, - destination_client_ip TEXT NULL, - destination_client_city TEXT NULL, - destination_client_country TEXT NULL, - - usd_value numeric(18, 9) NULL, - - spam_confidence numeric(18, 9), - - created_at timestamp with time zone NOT NULL - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_event; - ` -) - -var ( - testStore event.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestEventPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/event/record.go b/pkg/code/data/event/record.go deleted file mode 100644 index 26fef093..00000000 --- a/pkg/code/data/event/record.go +++ /dev/null @@ -1,180 +0,0 @@ -package event - -import ( - "errors" - "time" - - "github.com/code-payments/code-server/pkg/pointer" -) - -type EventType uint32 - -const ( - UnknownEvent EventType = iota - AccountCreated - WelcomeBonusClaimed - InPersonGrab - RemoteSend - MicroPayment - Withdrawal -) - -type Record struct { - Id uint64 - - // A common ID generally agreed upon. For example, a money movement flow using - // multiple intents may want to standardize on which intent ID is used to add - // additional metadata as it becomes available. - EventId string - EventType EventType - - // Involved accounts - SourceCodeAccount string - DestinationCodeAccount *string - ExternalTokenAccount *string - - // Involved identities - SourceIdentity string - DestinationIdentity *string - - // Involved IP addresses, and associated metadata - // - // todo: Requires MaxMind data set. IP metadata be missing until we do. - // todo: May add additional "interesting" IP metadata after reviewing MaxMind data set. - SourceClientIp *string - SourceClientCity *string - SourceClientCountry *string - DestinationClientIp *string - DestinationClientCity *string - DestinationClientCountry *string - - UsdValue *float64 - - // Could be a good way to trigger human reviews, or automated ban processes, - // for example. Initially, this will always be zero until we learn more about - // good rulesets. - SpamConfidence float64 // [0, 1] - - CreatedAt time.Time -} - -// todo: Per-event type validation than just the basic stuff defined here -func (r *Record) Validate() error { - if len(r.EventId) == 0 { - return errors.New("event id is required") - } - - if r.EventType == UnknownEvent || r.EventType > Withdrawal { - return errors.New("invalid event type") - } - - if len(r.SourceCodeAccount) == 0 { - return errors.New("source code account is required") - } - - if r.DestinationCodeAccount != nil && len(*r.DestinationCodeAccount) == 0 { - return errors.New("destination code account is required when set") - } - - if r.ExternalTokenAccount != nil && len(*r.ExternalTokenAccount) == 0 { - return errors.New("external token account is required when set") - } - - if len(r.SourceIdentity) == 0 { - return errors.New("source identity is required") - } - - if r.DestinationIdentity != nil && len(*r.DestinationIdentity) == 0 { - return errors.New("destination identity is required when set") - } - - if r.SourceClientIp != nil && len(*r.SourceClientIp) == 0 { - return errors.New("source client ip is required when set") - } - - if r.SourceClientCity != nil && len(*r.SourceClientCity) == 0 { - return errors.New("source client city is required when set") - } - - if r.SourceClientCountry != nil && len(*r.SourceClientCountry) == 0 { - return errors.New("source client country is required when set") - } - - if r.DestinationClientIp != nil && len(*r.DestinationClientIp) == 0 { - return errors.New("destination client ip is required when set") - } - - if r.DestinationClientCity != nil && len(*r.DestinationClientCity) == 0 { - return errors.New("destination client city is required when set") - } - - if r.DestinationClientCountry != nil && len(*r.DestinationClientCountry) == 0 { - return errors.New("destination client country is required when set") - } - - if r.UsdValue != nil && *r.UsdValue == 0 { - return errors.New("usd value is required when set") - } - - if r.SpamConfidence < 0 || r.SpamConfidence > 1 { - return errors.New("spam confidence must be in the range [0, 1]") - } - - return nil -} - -func (r *Record) Clone() Record { - return Record{ - Id: r.Id, - - EventId: r.EventId, - EventType: r.EventType, - - SourceCodeAccount: r.SourceCodeAccount, - DestinationCodeAccount: pointer.StringCopy(r.DestinationCodeAccount), - ExternalTokenAccount: pointer.StringCopy(r.ExternalTokenAccount), - - SourceIdentity: r.SourceIdentity, - DestinationIdentity: pointer.StringCopy(r.DestinationIdentity), - - SourceClientIp: pointer.StringCopy(r.SourceClientIp), - SourceClientCity: pointer.StringCopy(r.SourceClientCity), - SourceClientCountry: pointer.StringCopy(r.SourceClientCountry), - DestinationClientIp: pointer.StringCopy(r.DestinationClientIp), - DestinationClientCity: pointer.StringCopy(r.DestinationClientCity), - DestinationClientCountry: pointer.StringCopy(r.DestinationClientCountry), - - UsdValue: pointer.Float64Copy(r.UsdValue), - - SpamConfidence: r.SpamConfidence, - - CreatedAt: r.CreatedAt, - } -} - -func (r *Record) CopyTo(dst *Record) { - dst.Id = r.Id - - dst.EventId = r.EventId - dst.EventType = r.EventType - - dst.SourceCodeAccount = r.SourceCodeAccount - dst.DestinationCodeAccount = pointer.StringCopy(r.DestinationCodeAccount) - dst.ExternalTokenAccount = pointer.StringCopy(r.ExternalTokenAccount) - - dst.SourceIdentity = r.SourceIdentity - dst.DestinationIdentity = pointer.StringCopy(r.DestinationIdentity) - - dst.SourceClientIp = pointer.StringCopy(r.SourceClientIp) - dst.SourceClientCity = pointer.StringCopy(r.SourceClientCity) - dst.SourceClientCountry = pointer.StringCopy(r.SourceClientCountry) - dst.DestinationClientIp = pointer.StringCopy(r.DestinationClientIp) - dst.DestinationClientCity = pointer.StringCopy(r.DestinationClientCity) - dst.DestinationClientCountry = pointer.StringCopy(r.DestinationClientCountry) - - dst.UsdValue = pointer.Float64Copy(r.UsdValue) - - dst.SpamConfidence = r.SpamConfidence - - dst.CreatedAt = r.CreatedAt -} diff --git a/pkg/code/data/event/store.go b/pkg/code/data/event/store.go deleted file mode 100644 index 45506d7e..00000000 --- a/pkg/code/data/event/store.go +++ /dev/null @@ -1,21 +0,0 @@ -package event - -import ( - "context" - "errors" -) - -var ( - ErrEventNotFound = errors.New("event record not found") -) - -type Store interface { - // Save creates or updates an event record. For updates, only fields that can - // be reasonably changed or provided at a later time are supported. - Save(ctx context.Context, record *Record) error - - // Get gets an event record by its event ID - Get(ctx context.Context, id string) (*Record, error) - - // todo: Various other methods that can help us with product or spam tracking -} diff --git a/pkg/code/data/event/tests/tests.go b/pkg/code/data/event/tests/tests.go deleted file mode 100644 index 69e20bf5..00000000 --- a/pkg/code/data/event/tests/tests.go +++ /dev/null @@ -1,109 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/event" -) - -func RunTests(t *testing.T, s event.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s event.Store){ - testHappyPath, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s event.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - start := time.Now() - - expected := &event.Record{ - EventId: "event_id", - EventType: event.MicroPayment, - - SourceCodeAccount: "source_code_account", - DestinationCodeAccount: pointer.String("destination_code_account"), - ExternalTokenAccount: pointer.String("external_token_account"), - - SourceIdentity: "source_identity", - DestinationIdentity: pointer.String("destination_identity"), - - SourceClientIp: pointer.String("source_client_ip"), - SourceClientCity: pointer.String("source_client_city"), - SourceClientCountry: pointer.String("source_client_country"), - DestinationClientIp: pointer.String("destination_client_ip"), - DestinationClientCity: pointer.String("destination_client_city"), - DestinationClientCountry: pointer.String("destination_client_country"), - - UsdValue: pointer.Float64(123.456), - - SpamConfidence: 0.75, - } - - _, err := s.Get(ctx, expected.EventId) - assert.Equal(t, event.ErrEventNotFound, err) - - cloned := expected.Clone() - require.NoError(t, s.Save(ctx, expected)) - - assert.EqualValues(t, 1, expected.Id) - assert.True(t, expected.CreatedAt.After(start)) - assert.True(t, expected.CreatedAt.Before(start.Add(500*time.Millisecond))) - - actual, err := s.Get(ctx, cloned.EventId) - require.NoError(t, err) - assert.EqualValues(t, 1, actual.Id) - assert.EqualValues(t, expected.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assertEquivalentRecords(t, &cloned, actual) - - expected.DestinationCodeAccount = pointer.String("destination_code_account_updated") - expected.DestinationIdentity = pointer.String("destination_identity_updated") - expected.DestinationClientIp = pointer.String("destination_client_ip_updated") - expected.DestinationClientCity = pointer.String("destination_client_city_updated") - expected.DestinationClientCountry = pointer.String("destination_client_country_updated") - expected.SpamConfidence = 0.5 - - cloned = expected.Clone() - require.NoError(t, s.Save(ctx, expected)) - assert.EqualValues(t, 1, expected.Id) - - actual, err = s.Get(ctx, cloned.EventId) - require.NoError(t, err) - assert.EqualValues(t, 1, actual.Id) - assert.EqualValues(t, expected.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assertEquivalentRecords(t, &cloned, actual) - }) -} - -func assertEquivalentRecords(t *testing.T, obj1, obj2 *event.Record) { - assert.Equal(t, obj1.EventId, obj2.EventId) - assert.Equal(t, obj1.EventType, obj2.EventType) - - assert.Equal(t, obj1.SourceCodeAccount, obj2.SourceCodeAccount) - assert.EqualValues(t, obj1.DestinationCodeAccount, obj2.DestinationCodeAccount) - assert.EqualValues(t, obj1.ExternalTokenAccount, obj2.ExternalTokenAccount) - - assert.Equal(t, obj1.SourceIdentity, obj2.SourceIdentity) - assert.EqualValues(t, obj1.DestinationIdentity, obj2.DestinationIdentity) - - assert.EqualValues(t, obj1.SourceClientIp, obj2.SourceClientIp) - assert.EqualValues(t, obj1.SourceClientCity, obj2.SourceClientCity) - assert.EqualValues(t, obj1.SourceClientCountry, obj2.SourceClientCountry) - assert.EqualValues(t, obj1.DestinationClientIp, obj2.DestinationClientIp) - assert.EqualValues(t, obj1.DestinationClientCity, obj2.DestinationClientCity) - assert.EqualValues(t, obj1.DestinationClientCountry, obj2.DestinationClientCountry) - - assert.EqualValues(t, obj1.UsdValue, obj2.UsdValue) - - assert.Equal(t, obj1.SpamConfidence, obj2.SpamConfidence) -} diff --git a/pkg/code/data/external.go b/pkg/code/data/external.go index 5a4ab151..fe048019 100644 --- a/pkg/code/data/external.go +++ b/pkg/code/data/external.go @@ -5,11 +5,13 @@ import ( "errors" "time" + "github.com/code-payments/code-server/pkg/code/config" "github.com/code-payments/code-server/pkg/code/data/currency" currency_lib "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/currency/coingecko" "github.com/code-payments/code-server/pkg/currency/fixer" "github.com/code-payments/code-server/pkg/metrics" + "github.com/code-payments/code-server/pkg/usdc" ) const ( @@ -44,7 +46,7 @@ func (dp *WebProvider) GetCurrentExchangeRatesFromExternalProviders(ctx context. tracer := metrics.TraceMethodCall(ctx, webProviderMetricsName, "GetCurrentExchangeRatesFromExternalProviders") defer tracer.End() - coinGeckoData, err := dp.coinGecko.GetCurrentRates(ctx, string(currency_lib.KIN)) + coinGeckoData, err := dp.coinGecko.GetCurrentRates(ctx, string(config.CoreMintSymbol)) if err != nil { return nil, err } @@ -54,7 +56,7 @@ func (dp *WebProvider) GetCurrentExchangeRatesFromExternalProviders(ctx context. return nil, err } - rates, err := computeAllKinExchangeRates(coinGeckoData.Rates, fixerData.Rates) + rates, err := computeAllExchangeRates(coinGeckoData.Rates, fixerData.Rates) if err != nil { return nil, err } @@ -68,7 +70,7 @@ func (dp *WebProvider) GetPastExchangeRatesFromExternalProviders(ctx context.Con tracer := metrics.TraceMethodCall(ctx, webProviderMetricsName, "GetPastExchangeRatesFromExternalProviders") defer tracer.End() - coinGeckoData, err := dp.coinGecko.GetHistoricalRates(ctx, string(currency_lib.KIN), t.UTC()) + coinGeckoData, err := dp.coinGecko.GetHistoricalRates(ctx, string(config.CoreMintSymbol), t.UTC()) if err != nil { return nil, err } @@ -78,7 +80,7 @@ func (dp *WebProvider) GetPastExchangeRatesFromExternalProviders(ctx context.Con return nil, err } - rates, err := computeAllKinExchangeRates(coinGeckoData.Rates, fixerData.Rates) + rates, err := computeAllExchangeRates(coinGeckoData.Rates, fixerData.Rates) if err != nil { return nil, err } @@ -89,21 +91,20 @@ func (dp *WebProvider) GetPastExchangeRatesFromExternalProviders(ctx context.Con }, nil } -func computeAllKinExchangeRates(kinRates map[string]float64, usdRates map[string]float64) (map[string]float64, error) { - kinToUsd, ok := kinRates[string(currency_lib.USD)] +func computeAllExchangeRates(coreMintRates map[string]float64, usdRates map[string]float64) (map[string]float64, error) { + coreMintToUsd, ok := coreMintRates[string(currency_lib.USD)] if !ok { - return nil, errors.New("kin to usd rate missing") + return nil, errors.New("usd rate missing") + } + if config.CoreMintPublicKeyString == usdc.Mint { + coreMintToUsd = 1.0 } + res := make(map[string]float64) + res[string(currency_lib.USD)] = coreMintToUsd for symbol, usdRate := range usdRates { - // Trust the source of the crypto rate when available - if _, ok := kinRates[symbol]; ok { - continue - } - - kinExchangeRate := usdRate * kinToUsd - kinRates[symbol] = kinExchangeRate + coreExchangeRate := usdRate * coreMintToUsd + res[symbol] = coreExchangeRate } - - return kinRates, nil + return res, nil } diff --git a/pkg/code/data/external_test.go b/pkg/code/data/external_test.go index 8f178b77..93c376ff 100644 --- a/pkg/code/data/external_test.go +++ b/pkg/code/data/external_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/require" ) -func TestComputeAllKinExchangeRates_HappyPath(t *testing.T) { - kinRates := map[string]float64{ +func TestComputeAllExchangeRates_HappyPath(t *testing.T) { + coreMintRates := map[string]float64{ "usd": 0.5, "cad": 1.0, } @@ -20,16 +20,16 @@ func TestComputeAllKinExchangeRates_HappyPath(t *testing.T) { "aud": 0.66, } - rates, err := computeAllKinExchangeRates(kinRates, usdRates) + rates, err := computeAllExchangeRates(coreMintRates, usdRates) require.NoError(t, err) assert.Equal(t, rates["usd"], 0.5) - assert.Equal(t, rates["cad"], 1.0) // FX rate differs, but we always prefer the source kin rate + assert.Equal(t, rates["cad"], 0.65) assert.Equal(t, rates["eur"], 0.5) assert.Equal(t, rates["aud"], 0.33) } -func TestComputeAllKinExchangeRates_UsdRateMissing(t *testing.T) { +func TestComputeAllExchangeRates_UsdRateMissing(t *testing.T) { kinRates := map[string]float64{ "cad": 1.0, } @@ -41,6 +41,6 @@ func TestComputeAllKinExchangeRates_UsdRateMissing(t *testing.T) { "aud": 0.66, } - _, err := computeAllKinExchangeRates(kinRates, usdRates) + _, err := computeAllExchangeRates(kinRates, usdRates) assert.Error(t, err) } diff --git a/pkg/code/data/fulfillment/fulfillment.go b/pkg/code/data/fulfillment/fulfillment.go index 5936dfea..056f4929 100644 --- a/pkg/code/data/fulfillment/fulfillment.go +++ b/pkg/code/data/fulfillment/fulfillment.go @@ -7,7 +7,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/phone" "github.com/code-payments/code-server/pkg/pointer" ) @@ -24,17 +23,17 @@ const ( InitializeLockedTimelockAccount NoPrivacyTransferWithAuthority NoPrivacyWithdraw - TemporaryPrivacyTransferWithAuthority - PermanentPrivacyTransferWithAuthority - TransferWithCommitment - CloseEmptyTimelockAccount // Technically a compression with the new VM flows - CloseDormantTimelockAccount // Deprecated by the VM - SaveRecentRoot - InitializeCommitmentProof // Deprecated with new VM flows - UploadCommitmentProof // Deprecated with new VM flows - VerifyCommitmentProof // Deprecated with new VM flows - OpenCommitmentVault // Deprecated with new VM flows - CloseCommitment + TemporaryPrivacyTransferWithAuthority // Deprecated privacy flow + PermanentPrivacyTransferWithAuthority // Deprecated privacy flow + TransferWithCommitment // Deprecated privacy flow + CloseEmptyTimelockAccount // Technically a compression with the new VM flows + CloseDormantTimelockAccount // Deprecated by the VM + SaveRecentRoot // Deprecated privacy flow + InitializeCommitmentProof // Deprecated with new VM flows + UploadCommitmentProof // Deprecated with new VM flows + VerifyCommitmentProof // Deprecated with new VM flows + OpenCommitmentVault // Deprecated with new VM flows + CloseCommitment // Deprecated privacy flow ) type State uint8 @@ -88,9 +87,6 @@ type Record struct { // (eg. depedencies), so accidentally making some actively scheduled is ok. DisableActiveScheduling bool - // Metadata required to help make antispam decisions - InitiatorPhoneNumber *string - State State CreatedAt time.Time @@ -157,7 +153,6 @@ func (r *Record) Clone() Record { ActionOrderingIndex: r.ActionOrderingIndex, FulfillmentOrderingIndex: r.FulfillmentOrderingIndex, DisableActiveScheduling: r.DisableActiveScheduling, - InitiatorPhoneNumber: pointer.StringCopy(r.InitiatorPhoneNumber), State: r.State, CreatedAt: r.CreatedAt, } @@ -182,7 +177,6 @@ func (r *Record) CopyTo(dst *Record) { dst.IntentOrderingIndex = r.IntentOrderingIndex dst.ActionOrderingIndex = r.ActionOrderingIndex dst.FulfillmentOrderingIndex = r.FulfillmentOrderingIndex - dst.InitiatorPhoneNumber = r.InitiatorPhoneNumber dst.DisableActiveScheduling = r.DisableActiveScheduling dst.State = r.State dst.CreatedAt = r.CreatedAt @@ -251,10 +245,6 @@ func (r *Record) Validate() error { // todo: validate intent, action and fulfillment type all align - if r.InitiatorPhoneNumber != nil && !phone.IsE164Format(*r.InitiatorPhoneNumber) { - return errors.New("initiator phone number doesn't match E.164 format") - } - return nil } diff --git a/pkg/code/data/fulfillment/memory/store.go b/pkg/code/data/fulfillment/memory/store.go index 0d64ebec..c14c0f02 100644 --- a/pkg/code/data/fulfillment/memory/store.go +++ b/pkg/code/data/fulfillment/memory/store.go @@ -538,43 +538,6 @@ func (s *store) MarkAsActivelyScheduled(ctx context.Context, id uint64) error { return nil } -func (s *store) ActivelyScheduleTreasuryAdvances(ctx context.Context, treasury string, intentOrderingIndex uint64, limit int) (uint64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - var updateCount uint64 - for _, data := range s.records { - if !data.DisableActiveScheduling { - continue - } - - if data.State != fulfillment.StateUnknown { - continue - } - - if data.FulfillmentType != fulfillment.TransferWithCommitment { - continue - } - - if data.Source != treasury { - continue - } - - if data.IntentOrderingIndex >= intentOrderingIndex { - continue - } - - data.DisableActiveScheduling = false - - updateCount += 1 - if updateCount >= uint64(limit) { - return updateCount, nil - } - } - - return updateCount, nil -} - func (s *store) GetById(ctx context.Context, id uint64) (*fulfillment.Record, error) { if id == 0 { return nil, fulfillment.ErrFulfillmentNotFound diff --git a/pkg/code/data/fulfillment/postgres/model.go b/pkg/code/data/fulfillment/postgres/model.go index bcc6667c..2b3fea6b 100644 --- a/pkg/code/data/fulfillment/postgres/model.go +++ b/pkg/code/data/fulfillment/postgres/model.go @@ -40,7 +40,6 @@ type fulfillmentModel struct { ActionOrderingIndex uint32 `db:"action_ordering_index"` FulfillmentOrderingIndex uint32 `db:"fulfillment_ordering_index"` DisableActiveScheduling bool `db:"disable_active_scheduling"` - InitiatorPhoneNumber sql.NullString `db:"phone_number"` // todo: rename the DB field State uint `db:"state"` CreatedAt time.Time `db:"created_at"` BatchInsertionId int `db:"batch_insertion_id"` @@ -97,12 +96,6 @@ func toFulfillmentModel(obj *fulfillment.Record) (*fulfillmentModel, error) { destinationValue.String = *obj.Destination } - var initiatorPhoneNumberValue sql.NullString - if obj.InitiatorPhoneNumber != nil { - initiatorPhoneNumberValue.Valid = true - initiatorPhoneNumberValue.String = *obj.InitiatorPhoneNumber - } - return &fulfillmentModel{ Id: int64(obj.Id), Intent: obj.Intent, @@ -123,7 +116,6 @@ func toFulfillmentModel(obj *fulfillment.Record) (*fulfillmentModel, error) { ActionOrderingIndex: obj.ActionOrderingIndex, FulfillmentOrderingIndex: obj.FulfillmentOrderingIndex, DisableActiveScheduling: obj.DisableActiveScheduling, - InitiatorPhoneNumber: initiatorPhoneNumberValue, State: uint(obj.State), CreatedAt: obj.CreatedAt, }, nil @@ -165,11 +157,6 @@ func fromFulfillmentModel(obj *fulfillmentModel) *fulfillment.Record { destination = &obj.Destination.String } - var initiatorPhoneNumber *string - if obj.InitiatorPhoneNumber.Valid { - initiatorPhoneNumber = &obj.InitiatorPhoneNumber.String - } - return &fulfillment.Record{ Id: uint64(obj.Id), Intent: obj.Intent, @@ -190,7 +177,6 @@ func fromFulfillmentModel(obj *fulfillmentModel) *fulfillment.Record { ActionOrderingIndex: obj.ActionOrderingIndex, FulfillmentOrderingIndex: obj.FulfillmentOrderingIndex, DisableActiveScheduling: obj.DisableActiveScheduling, - InitiatorPhoneNumber: initiatorPhoneNumber, State: fulfillment.State(obj.State), CreatedAt: obj.CreatedAt.UTC(), } @@ -402,7 +388,7 @@ func (m *fulfillmentModel) dbUpdate(ctx context.Context, db *sqlx.DB) error { SET signature = $2, nonce = $3, blockhash = $4, data = $5, state = $6, virtual_signature = $7, virtual_nonce = $8, virtual_blockhash = $9%s WHERE id = $1 RETURNING - id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at`, + id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at`, preSortingUpdateStmt, ) @@ -420,7 +406,7 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*fulfillmentModel) var res []*fulfillmentModel query := `WITH inserted AS (` - query += `INSERT INTO ` + fulfillmentTableName + ` (intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at, batch_insertion_id) VALUES ` + query += `INSERT INTO ` + fulfillmentTableName + ` (intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at, batch_insertion_id) VALUES ` var parameters []interface{} for i, model := range models { @@ -434,8 +420,8 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*fulfillmentModel) baseIndex := len(parameters) query += fmt.Sprintf( - `($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d)`, - baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7, baseIndex+8, baseIndex+9, baseIndex+10, baseIndex+11, baseIndex+12, baseIndex+13, baseIndex+14, baseIndex+15, baseIndex+16, baseIndex+17, baseIndex+18, baseIndex+19, baseIndex+20, baseIndex+21, baseIndex+22, + `($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d)`, + baseIndex+1, baseIndex+2, baseIndex+3, baseIndex+4, baseIndex+5, baseIndex+6, baseIndex+7, baseIndex+8, baseIndex+9, baseIndex+10, baseIndex+11, baseIndex+12, baseIndex+13, baseIndex+14, baseIndex+15, baseIndex+16, baseIndex+17, baseIndex+18, baseIndex+19, baseIndex+20, baseIndex+21, ) if i != len(models)-1 { @@ -464,14 +450,13 @@ func dbPutAllInTx(ctx context.Context, tx *sqlx.Tx, models []*fulfillmentModel) model.ActionOrderingIndex, model.FulfillmentOrderingIndex, model.DisableActiveScheduling, - model.InitiatorPhoneNumber, model.State, model.CreatedAt, batchInsertionId, ) } - query += ` RETURNING id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at, batch_insertion_id) ` + query += ` RETURNING id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at, batch_insertion_id) ` // Kind of hacky, but we don't really have a great PK for on demand transactions // that allows us to update the corresponding record that was passed in (for example, @@ -514,35 +499,6 @@ func dbMarkAsActivelyScheduled(ctx context.Context, db *sqlx.DB, id uint64) erro return nil } -func dbActivelyScheduleTreasuryAdvances(ctx context.Context, db *sqlx.DB, treasury string, intentOrderingIndex uint64, limit int) (uint64, error) { - query := `UPDATE ` + fulfillmentTableName + ` - SET disable_active_scheduling = false - WHERE id IN ( - SELECT id FROM ` + fulfillmentTableName + ` - WHERE source = $1 AND state = $2 AND intent_ordering_index < $3 AND fulfillment_type = $4 AND disable_active_scheduling IS TRUE - LIMIT $5 - FOR UPDATE - )` - res, err := db.ExecContext( - ctx, - query, - treasury, - fulfillment.StateUnknown, - intentOrderingIndex, - fulfillment.TransferWithCommitment, - limit, - ) - if err != nil { - return 0, err - } - - rowsAffected, err := res.RowsAffected() - if err != nil { - return 0, err - } - return uint64(rowsAffected), nil -} - func dbGetById(ctx context.Context, db *sqlx.DB, id uint64) (*fulfillmentModel, error) { if id == 0 { return nil, fulfillment.ErrFulfillmentNotFound @@ -550,7 +506,7 @@ func dbGetById(ctx context.Context, db *sqlx.DB, id uint64) (*fulfillmentModel, res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE id = $1 LIMIT 1` @@ -569,7 +525,7 @@ func dbGetBySignature(ctx context.Context, db *sqlx.DB, signature string) (*fulf res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE signature = $1 LIMIT 1` @@ -588,7 +544,7 @@ func dbGetByVirtualSignature(ctx context.Context, db *sqlx.DB, signature string) res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE virtual_signature = $1 LIMIT 1` @@ -603,7 +559,7 @@ func dbGetByVirtualSignature(ctx context.Context, db *sqlx.DB, signature string) func dbGetAllByState(ctx context.Context, db *sqlx.DB, state fulfillment.State, includeDisabledActiveScheduling bool, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*fulfillmentModel, error) { res := []*fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE (state = $1 AND %s) ` @@ -632,7 +588,7 @@ func dbGetAllByState(ctx context.Context, db *sqlx.DB, state fulfillment.State, func dbGetAllByIntent(ctx context.Context, db *sqlx.DB, intent string, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*fulfillmentModel, error) { res := []*fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE (intent = $1) ` @@ -655,7 +611,7 @@ func dbGetAllByIntent(ctx context.Context, db *sqlx.DB, intent string, cursor q. func dbGetAllByAction(ctx context.Context, db *sqlx.DB, intentId string, actionId uint32) ([]*fulfillmentModel, error) { res := []*fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE (intent = $1 and action_id = $2) ` @@ -675,7 +631,7 @@ func dbGetAllByAction(ctx context.Context, db *sqlx.DB, intentId string, actionI func dbGetAllByTypeAndAction(ctx context.Context, db *sqlx.DB, fulfillmentType fulfillment.Type, intentId string, actionId uint32) ([]*fulfillmentModel, error) { res := []*fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE intent = $1 AND action_id = $2 AND fulfillment_type = $3 ` @@ -695,7 +651,7 @@ func dbGetAllByTypeAndAction(ctx context.Context, db *sqlx.DB, fulfillmentType f func dbGetFirstSchedulableByAddressAsSource(ctx context.Context, db *sqlx.DB, address string) (*fulfillmentModel, error) { res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE (source = $1 AND (state = $2 OR state = $3)) ORDER BY intent_ordering_index ASC, action_ordering_index ASC, fulfillment_ordering_index ASC @@ -711,7 +667,7 @@ func dbGetFirstSchedulableByAddressAsSource(ctx context.Context, db *sqlx.DB, ad func dbGetFirstSchedulableByAddressAsDestination(ctx context.Context, db *sqlx.DB, address string) (*fulfillmentModel, error) { res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE (destination = $1 AND (state = $2 OR state = $3)) ORDER BY intent_ordering_index ASC, action_ordering_index ASC, fulfillment_ordering_index ASC @@ -727,7 +683,7 @@ func dbGetFirstSchedulableByAddressAsDestination(ctx context.Context, db *sqlx.D func dbGetFirstSchedulableByType(ctx context.Context, db *sqlx.DB, fulfillmentType fulfillment.Type) (*fulfillmentModel, error) { res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE (fulfillment_type = $1 AND (state = $2 OR state = $3)) ORDER BY intent_ordering_index ASC, action_ordering_index ASC, fulfillment_ordering_index ASC @@ -743,7 +699,7 @@ func dbGetFirstSchedulableByType(ctx context.Context, db *sqlx.DB, fulfillmentTy func dbGetNextSchedulableByAddress(ctx context.Context, db *sqlx.DB, address string, intentOrderingIndex uint64, actionOrderingIndex, fulfillmentOrderingIndex uint32) (*fulfillmentModel, error) { res := &fulfillmentModel{} - query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, phone_number, state, created_at + query := `SELECT id, intent, intent_type, action_id, action_type, fulfillment_type, data, signature, nonce, blockhash, virtual_signature, virtual_nonce, virtual_blockhash, source, destination, intent_ordering_index, action_ordering_index, fulfillment_ordering_index, disable_active_scheduling, state, created_at FROM ` + fulfillmentTableName + ` WHERE ((source = $1 OR destination = $1) AND (state = $2 OR state = $3) AND (intent_ordering_index > $4 OR (intent_ordering_index = $4 AND action_ordering_index > $5) OR (intent_ordering_index = $4 AND action_ordering_index = $5 AND fulfillment_ordering_index > $6))) ORDER BY intent_ordering_index ASC, action_ordering_index ASC, fulfillment_ordering_index ASC diff --git a/pkg/code/data/fulfillment/postgres/store.go b/pkg/code/data/fulfillment/postgres/store.go index c33bd280..f6d151a8 100644 --- a/pkg/code/data/fulfillment/postgres/store.go +++ b/pkg/code/data/fulfillment/postgres/store.go @@ -131,11 +131,6 @@ func (s *store) MarkAsActivelyScheduled(ctx context.Context, id uint64) error { return dbMarkAsActivelyScheduled(ctx, s.db, id) } -// ActivelyScheduleTreasuryAdvances implements fulfillment.Store.ActivelyScheduleTreasuryAdvances -func (s *store) ActivelyScheduleTreasuryAdvances(ctx context.Context, treasury string, intentOrderingIndex uint64, limit int) (uint64, error) { - return dbActivelyScheduleTreasuryAdvances(ctx, s.db, treasury, intentOrderingIndex, limit) -} - // GetById implements fulfillment.Store.GetById func (s *store) GetById(ctx context.Context, id uint64) (*fulfillment.Record, error) { obj, err := dbGetById(ctx, s.db, id) diff --git a/pkg/code/data/fulfillment/postgres/store_test.go b/pkg/code/data/fulfillment/postgres/store_test.go index 39da7720..13530cd6 100644 --- a/pkg/code/data/fulfillment/postgres/store_test.go +++ b/pkg/code/data/fulfillment/postgres/store_test.go @@ -48,8 +48,6 @@ const ( disable_active_scheduling BOOL NOT NULL, - phone_number TEXT NULL, - state INTEGER NOT NULL, batch_insertion_id INTEGER NOT NULL, diff --git a/pkg/code/data/fulfillment/store.go b/pkg/code/data/fulfillment/store.go index 026ad7b5..fbe0b097 100644 --- a/pkg/code/data/fulfillment/store.go +++ b/pkg/code/data/fulfillment/store.go @@ -65,11 +65,6 @@ type Store interface { // MarkAsActivelyScheduled marks a fulfillment as actively scheduled MarkAsActivelyScheduled(ctx context.Context, id uint64) error - // ActivelyScheduleTreasuryAdvances is a specialized MarkAsActivelyScheduled variant - // to batch enable active scheduling for treasury advances at a particular point in time - // defined by the intent ordering index. - ActivelyScheduleTreasuryAdvances(ctx context.Context, treasury string, intentOrderingIndex uint64, limit int) (uint64, error) - // GetAllByState returns all fulfillment records for a given state. // // Returns ErrNotFound if no records are found. diff --git a/pkg/code/data/fulfillment/tests/tests.go b/pkg/code/data/fulfillment/tests/tests.go index 9f33ed82..8da985fa 100644 --- a/pkg/code/data/fulfillment/tests/tests.go +++ b/pkg/code/data/fulfillment/tests/tests.go @@ -29,7 +29,6 @@ func RunTests(t *testing.T, s fulfillment.Store, teardown func()) { testGetCount, testSchedulingQueries, testSubsidizerQueries, - testTreasuryQueries, } { tf(t, s) teardown() @@ -69,7 +68,6 @@ func testRoundTrip(t *testing.T, s fulfillment.Store) { ActionOrderingIndex: 2, FulfillmentOrderingIndex: 3, DisableActiveScheduling: false, - InitiatorPhoneNumber: pointer.String("+12223334444"), State: fulfillment.StateConfirmed, CreatedAt: time.Now(), } @@ -120,7 +118,6 @@ func testRoundTrip(t *testing.T, s fulfillment.Store) { ActionOrderingIndex: 2, FulfillmentOrderingIndex: 3, DisableActiveScheduling: true, - InitiatorPhoneNumber: nil, State: fulfillment.StateUnknown, CreatedAt: time.Now(), } @@ -165,7 +162,6 @@ func testBatchPut(t *testing.T, s fulfillment.Store) { ActionOrderingIndex: uint32(i), FulfillmentOrderingIndex: uint32(i), DisableActiveScheduling: false, - InitiatorPhoneNumber: pointer.String(fmt.Sprintf("+1800555%d", i)), State: fulfillment.StateConfirmed, CreatedAt: time.Now(), } @@ -244,7 +240,6 @@ func testUpdate(t *testing.T, s fulfillment.Store) { Blockhash: nil, Source: "test_source", Destination: nil, - InitiatorPhoneNumber: pointer.String("+12223334444"), IntentOrderingIndex: 1, ActionOrderingIndex: 2, FulfillmentOrderingIndex: 3, @@ -935,133 +930,6 @@ func testSubsidizerQueries(t *testing.T, s fulfillment.Store) { }) } -func testTreasuryQueries(t *testing.T, s fulfillment.Store) { - t.Run("testTreasuryQueries", func(t *testing.T) { - ctx := context.Background() - - records := []*fulfillment.Record{ - // Everything is a candidate for update - {Source: "treasury1", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 1}, - {Source: "treasury1", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 2}, - {Source: "treasury1", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 3}, - {Source: "treasury1", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 4}, - {Source: "treasury1", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 5}, - - // Everything is a candidate for update - {Source: "treasury2", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 1}, - {Source: "treasury2", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 2}, - {Source: "treasury2", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 3}, - {Source: "treasury2", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 4}, - {Source: "treasury2", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 5}, - - // Everything is a candidate for update - {Source: "treasury3", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 1}, - {Source: "treasury3", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 2}, - {Source: "treasury3", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 3}, - {Source: "treasury3", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 4}, - {Source: "treasury3", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 5}, - - // Multiple states - {Source: "treasury4", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 1}, - {Source: "treasury4", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StatePending, DisableActiveScheduling: true, IntentOrderingIndex: 2}, - {Source: "treasury4", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateConfirmed, DisableActiveScheduling: true, IntentOrderingIndex: 3}, - {Source: "treasury4", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateFailed, DisableActiveScheduling: true, IntentOrderingIndex: 4}, - {Source: "treasury4", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateRevoked, DisableActiveScheduling: true, IntentOrderingIndex: 5}, - - // Multiple fulfillment thypes - {Source: "treasury5", FulfillmentType: fulfillment.TransferWithCommitment, State: fulfillment.StateUnknown, DisableActiveScheduling: true, IntentOrderingIndex: 1}, - {Source: "treasury5", FulfillmentType: fulfillment.SaveRecentRoot, State: fulfillment.StatePending, DisableActiveScheduling: true, IntentOrderingIndex: 2}, - {Source: "treasury5", FulfillmentType: fulfillment.InitializeLockedTimelockAccount, State: fulfillment.StateConfirmed, DisableActiveScheduling: true, IntentOrderingIndex: 3}, - {Source: "treasury5", FulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, State: fulfillment.StateFailed, DisableActiveScheduling: true, IntentOrderingIndex: 4}, - {Source: "treasury5", FulfillmentType: fulfillment.TemporaryPrivacyTransferWithAuthority, State: fulfillment.StateRevoked, DisableActiveScheduling: true, IntentOrderingIndex: 5}, - } - - // Fill in required fields that have no relevancy to this test - for i, record := range records { - record.Intent = fmt.Sprintf("i%d", i+1) - record.IntentType = intent.SendPrivatePayment - record.ActionType = action.PrivateTransfer - } - require.NoError(t, s.PutAll(ctx, records...)) - - // Everything is updated - updateCount, err := s.ActivelyScheduleTreasuryAdvances(ctx, "treasury1", 10, 10) - require.NoError(t, err) - assert.EqualValues(t, 5, updateCount) - for _, record := range records { - actual, err := s.GetById(ctx, record.Id) - require.NoError(t, err) - assert.Equal(t, record.Source == "treasury1", !actual.DisableActiveScheduling) - } - - // Everything updated, so should get an empty count without error - updateCount, err = s.ActivelyScheduleTreasuryAdvances(ctx, "treasury1", 10, 10) - require.NoError(t, err) - assert.EqualValues(t, 0, updateCount) - - // Subset matching intent ordering index is updated - updateCount, err = s.ActivelyScheduleTreasuryAdvances(ctx, "treasury2", 3, 10) - require.NoError(t, err) - assert.EqualValues(t, 2, updateCount) - for _, record := range records { - if record.Source != "treasury2" { - continue - } - - actual, err := s.GetById(ctx, record.Id) - require.NoError(t, err) - assert.Equal(t, record.IntentOrderingIndex < 3, !actual.DisableActiveScheduling) - } - - // Subset limited by limit value is updated - updateCount, err = s.ActivelyScheduleTreasuryAdvances(ctx, "treasury3", 10, 2) - require.NoError(t, err) - assert.EqualValues(t, 2, updateCount) - var totalUpdated int - for _, record := range records { - if record.Source != "treasury3" { - continue - } - - actual, err := s.GetById(ctx, record.Id) - require.NoError(t, err) - - if !actual.DisableActiveScheduling { - totalUpdated += 1 - } - } - assert.Equal(t, 2, totalUpdated) - - // Subset matching expected state is updated - updateCount, err = s.ActivelyScheduleTreasuryAdvances(ctx, "treasury4", 10, 10) - require.NoError(t, err) - assert.EqualValues(t, 1, updateCount) - for _, record := range records { - if record.Source != "treasury4" { - continue - } - - actual, err := s.GetById(ctx, record.Id) - require.NoError(t, err) - assert.Equal(t, record.State == fulfillment.StateUnknown, !actual.DisableActiveScheduling) - } - - // Subset matching expected fulfillment type is updated - updateCount, err = s.ActivelyScheduleTreasuryAdvances(ctx, "treasury5", 10, 10) - require.NoError(t, err) - assert.EqualValues(t, 1, updateCount) - for _, record := range records { - if record.Source != "treasury5" { - continue - } - - actual, err := s.GetById(ctx, record.Id) - require.NoError(t, err) - assert.Equal(t, record.FulfillmentType == fulfillment.TransferWithCommitment, !actual.DisableActiveScheduling) - } - }) -} - func assertEquivalentRecords(t *testing.T, obj1, obj2 *fulfillment.Record) { assert.Equal(t, obj1.Intent, obj2.Intent) assert.Equal(t, obj1.IntentType, obj2.IntentType) @@ -1081,7 +949,6 @@ func assertEquivalentRecords(t *testing.T, obj1, obj2 *fulfillment.Record) { assert.Equal(t, obj1.ActionOrderingIndex, obj2.ActionOrderingIndex) assert.Equal(t, obj1.FulfillmentOrderingIndex, obj2.FulfillmentOrderingIndex) assert.Equal(t, obj1.DisableActiveScheduling, obj2.DisableActiveScheduling) - assert.EqualValues(t, obj1.InitiatorPhoneNumber, obj2.InitiatorPhoneNumber) assert.Equal(t, obj1.State, obj2.State) assert.Equal(t, obj1.CreatedAt.Unix(), obj2.CreatedAt.Unix()) } diff --git a/pkg/code/data/intent/intent.go b/pkg/code/data/intent/intent.go index 6c7f5206..32965495 100644 --- a/pkg/code/data/intent/intent.go +++ b/pkg/code/data/intent/intent.go @@ -4,10 +4,7 @@ import ( "errors" "time" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/phone" ) var ( @@ -29,19 +26,19 @@ const ( type Type uint8 const ( - UnknownType Type = iota - LegacyPayment - LegacyCreateAccount + UnknownType Type = iota + LegacyPayment // Deprecated pre-2022 privacy flow + LegacyCreateAccount // Deprecated pre-2022 privacy flow OpenAccounts - SendPrivatePayment - ReceivePaymentsPrivately - SaveRecentRoot - MigrateToPrivacy2022 + SendPrivatePayment // Deprecated privacy flow + ReceivePaymentsPrivately // Deprecated privacy flow + SaveRecentRoot // Deprecated privacy flow + MigrateToPrivacy2022 // Deprecated privacy flow ExternalDeposit SendPublicPayment ReceivePaymentsPublicly - EstablishRelationship - Login + EstablishRelationship // Deprecated privacy flow + Login // Deprecated login flow ) type Record struct { @@ -51,95 +48,23 @@ type Record struct { IntentType Type InitiatorOwnerAccount string - InitiatorPhoneNumber *string - - // Intents v2 metadatum - OpenAccountsMetadata *OpenAccountsMetadata - SendPrivatePaymentMetadata *SendPrivatePaymentMetadata - ReceivePaymentsPrivatelyMetadata *ReceivePaymentsPrivatelyMetadata - SaveRecentRootMetadata *SaveRecentRootMetadata - MigrateToPrivacy2022Metadata *MigrateToPrivacy2022Metadata - ExternalDepositMetadata *ExternalDepositMetadata - SendPublicPaymentMetadata *SendPublicPaymentMetadata - ReceivePaymentsPubliclyMetadata *ReceivePaymentsPubliclyMetadata - EstablishRelationshipMetadata *EstablishRelationshipMetadata - LoginMetadata *LoginMetadata - - // Deprecated intents v1 metadatum - MoneyTransferMetadata *MoneyTransferMetadata - AccountManagementMetadata *AccountManagementMetadata - - State State - - CreatedAt time.Time -} - -type MoneyTransferMetadata struct { - Source string - Destination string - Quantity uint64 + OpenAccountsMetadata *OpenAccountsMetadata + ExternalDepositMetadata *ExternalDepositMetadata + SendPublicPaymentMetadata *SendPublicPaymentMetadata + ReceivePaymentsPubliclyMetadata *ReceivePaymentsPubliclyMetadata - ExchangeCurrency currency.Code - ExchangeRate float64 - UsdMarketValue float64 + ExtendedMetadata []byte - IsWithdrawal bool -} + State State -type AccountManagementMetadata struct { - TokenAccount string + CreatedAt time.Time } type OpenAccountsMetadata struct { // Nothing yet } -type SendPrivatePaymentMetadata struct { - DestinationOwnerAccount string - DestinationTokenAccount string - Quantity uint64 - - ExchangeCurrency currency.Code - ExchangeRate float64 - NativeAmount float64 - UsdMarketValue float64 - - IsWithdrawal bool - IsRemoteSend bool - IsMicroPayment bool - IsTip bool - IsChat bool - - // Set when IsTip = true - TipMetadata *TipMetadata - - // Set when IsChat = true - ChatId string -} - -type TipMetadata struct { - Platform transactionpb.TippedUser_Platform - Username string -} - -type ReceivePaymentsPrivatelyMetadata struct { - Source string - Quantity uint64 - IsDeposit bool - - UsdMarketValue float64 -} - -type SaveRecentRootMetadata struct { - TreasuryPool string - PreviousMostRecentRoot string -} - -type MigrateToPrivacy2022Metadata struct { - Quantity uint64 -} - type ExternalDepositMetadata struct { DestinationOwnerAccount string DestinationTokenAccount string @@ -180,62 +105,17 @@ type ReceivePaymentsPubliclyMetadata struct { UsdMarketValue float64 } -type EstablishRelationshipMetadata struct { - RelationshipTo string -} - -type LoginMetadata struct { - App string - UserId string -} - func (r *Record) IsCompleted() bool { return r.State == StateConfirmed } func (r *Record) Clone() Record { - var moneyTransferMetadata *MoneyTransferMetadata - if r.MoneyTransferMetadata != nil { - cloned := r.MoneyTransferMetadata.Clone() - moneyTransferMetadata = &cloned - } - - var accountManagementMetadata *AccountManagementMetadata - if r.AccountManagementMetadata != nil { - cloned := r.AccountManagementMetadata.Clone() - accountManagementMetadata = &cloned - } - var openAccountsMetadata *OpenAccountsMetadata if r.OpenAccountsMetadata != nil { cloned := r.OpenAccountsMetadata.Clone() openAccountsMetadata = &cloned } - var sendPrivatePaymentMetadata *SendPrivatePaymentMetadata - if r.SendPrivatePaymentMetadata != nil { - cloned := r.SendPrivatePaymentMetadata.Clone() - sendPrivatePaymentMetadata = &cloned - } - - var receivePaymentsPrivatelyMetadata *ReceivePaymentsPrivatelyMetadata - if r.ReceivePaymentsPrivatelyMetadata != nil { - cloned := r.ReceivePaymentsPrivatelyMetadata.Clone() - receivePaymentsPrivatelyMetadata = &cloned - } - - var saveRecentRootMetadata *SaveRecentRootMetadata - if r.SaveRecentRootMetadata != nil { - cloned := r.SaveRecentRootMetadata.Clone() - saveRecentRootMetadata = &cloned - } - - var migrateToPrivacy2022Metadata *MigrateToPrivacy2022Metadata - if r.MigrateToPrivacy2022Metadata != nil { - cloned := r.MigrateToPrivacy2022Metadata.Clone() - migrateToPrivacy2022Metadata = &cloned - } - var externalDepositMetadata *ExternalDepositMetadata if r.ExternalDepositMetadata != nil { cloned := r.ExternalDepositMetadata.Clone() @@ -254,24 +134,6 @@ func (r *Record) Clone() Record { receivePaymentsPubliclyMetadata = &cloned } - var establishRelationshipMetadata *EstablishRelationshipMetadata - if r.EstablishRelationshipMetadata != nil { - cloned := r.EstablishRelationshipMetadata.Clone() - establishRelationshipMetadata = &cloned - } - - var loginMetadata *LoginMetadata - if r.LoginMetadata != nil { - cloned := r.LoginMetadata.Clone() - loginMetadata = &cloned - } - - var initiatorPhoneNumber *string - if r.InitiatorPhoneNumber != nil { - value := *r.InitiatorPhoneNumber - initiatorPhoneNumber = &value - } - return Record{ Id: r.Id, @@ -279,21 +141,13 @@ func (r *Record) Clone() Record { IntentType: r.IntentType, InitiatorOwnerAccount: r.InitiatorOwnerAccount, - InitiatorPhoneNumber: initiatorPhoneNumber, - - OpenAccountsMetadata: openAccountsMetadata, - SendPrivatePaymentMetadata: sendPrivatePaymentMetadata, - ReceivePaymentsPrivatelyMetadata: receivePaymentsPrivatelyMetadata, - SaveRecentRootMetadata: saveRecentRootMetadata, - MigrateToPrivacy2022Metadata: migrateToPrivacy2022Metadata, - ExternalDepositMetadata: externalDepositMetadata, - SendPublicPaymentMetadata: sendPublicPaymentMetadata, - ReceivePaymentsPubliclyMetadata: receivePaymentsPubliclyMetadata, - EstablishRelationshipMetadata: establishRelationshipMetadata, - LoginMetadata: loginMetadata, - - MoneyTransferMetadata: moneyTransferMetadata, - AccountManagementMetadata: accountManagementMetadata, + + OpenAccountsMetadata: openAccountsMetadata, + ExternalDepositMetadata: externalDepositMetadata, + SendPublicPaymentMetadata: sendPublicPaymentMetadata, + ReceivePaymentsPubliclyMetadata: receivePaymentsPubliclyMetadata, + + ExtendedMetadata: r.ExtendedMetadata, State: r.State, @@ -308,20 +162,13 @@ func (r *Record) CopyTo(dst *Record) { dst.IntentType = r.IntentType dst.InitiatorOwnerAccount = r.InitiatorOwnerAccount - dst.InitiatorPhoneNumber = r.InitiatorPhoneNumber dst.OpenAccountsMetadata = r.OpenAccountsMetadata - dst.SendPrivatePaymentMetadata = r.SendPrivatePaymentMetadata - dst.ReceivePaymentsPrivatelyMetadata = r.ReceivePaymentsPrivatelyMetadata - dst.SaveRecentRootMetadata = r.SaveRecentRootMetadata - dst.MigrateToPrivacy2022Metadata = r.MigrateToPrivacy2022Metadata + dst.ExternalDepositMetadata = r.ExternalDepositMetadata dst.SendPublicPaymentMetadata = r.SendPublicPaymentMetadata dst.ReceivePaymentsPubliclyMetadata = r.ReceivePaymentsPubliclyMetadata - dst.EstablishRelationshipMetadata = r.EstablishRelationshipMetadata - dst.LoginMetadata = r.LoginMetadata - dst.MoneyTransferMetadata = r.MoneyTransferMetadata - dst.AccountManagementMetadata = r.AccountManagementMetadata + dst.ExtendedMetadata = r.ExtendedMetadata dst.State = r.State @@ -341,32 +188,6 @@ func (r *Record) Validate() error { return errors.New("initiator owner account is required") } - if r.InitiatorPhoneNumber != nil && !phone.IsE164Format(*r.InitiatorPhoneNumber) { - return errors.New("initiator phone number doesn't match E.164 format") - } - - if r.IntentType == LegacyPayment { - if r.MoneyTransferMetadata == nil { - return errors.New("money transfer metadata must be present") - } - - err := r.MoneyTransferMetadata.Validate() - if err != nil { - return err - } - } - - if r.IntentType == LegacyCreateAccount { - if r.AccountManagementMetadata == nil { - return errors.New("account management metadata must be present") - } - - err := r.AccountManagementMetadata.Validate() - if err != nil { - return err - } - } - if r.IntentType == OpenAccounts { if r.OpenAccountsMetadata == nil { return errors.New("open accounts metadata must be present") @@ -378,50 +199,6 @@ func (r *Record) Validate() error { } } - if r.IntentType == SendPrivatePayment { - if r.SendPrivatePaymentMetadata == nil { - return errors.New("send private payment metadata must be present") - } - - err := r.SendPrivatePaymentMetadata.Validate() - if err != nil { - return err - } - } - - if r.IntentType == ReceivePaymentsPrivately { - if r.ReceivePaymentsPrivatelyMetadata == nil { - return errors.New("receive payments privately metadata must be present") - } - - err := r.ReceivePaymentsPrivatelyMetadata.Validate() - if err != nil { - return err - } - } - - if r.IntentType == SaveRecentRoot { - if r.SaveRecentRootMetadata == nil { - return errors.New("save recent root metadata must be present") - } - - err := r.SaveRecentRootMetadata.Validate() - if err != nil { - return err - } - } - - if r.IntentType == MigrateToPrivacy2022 { - if r.MigrateToPrivacy2022Metadata == nil { - return errors.New("migrate to privacy 2022 metadata must be present") - } - - err := r.MigrateToPrivacy2022Metadata.Validate() - if err != nil { - return err - } - } - if r.IntentType == ExternalDeposit { if r.ExternalDepositMetadata == nil { return errors.New("external deposit metadata must be present") @@ -455,96 +232,6 @@ func (r *Record) Validate() error { } } - if r.IntentType == EstablishRelationship { - if r.EstablishRelationshipMetadata == nil { - return errors.New("establish relationship metadata must be present") - } - - err := r.EstablishRelationshipMetadata.Validate() - if err != nil { - return err - } - } - - if r.IntentType == Login { - if r.LoginMetadata == nil { - return errors.New("login metadata must be present") - } - - err := r.LoginMetadata.Validate() - if err != nil { - return err - } - } - - return nil -} - -func (m *MoneyTransferMetadata) Clone() MoneyTransferMetadata { - return MoneyTransferMetadata{ - Source: m.Source, - Destination: m.Destination, - Quantity: m.Quantity, - ExchangeCurrency: m.ExchangeCurrency, - ExchangeRate: m.ExchangeRate, - UsdMarketValue: m.UsdMarketValue, - IsWithdrawal: m.IsWithdrawal, - } -} - -func (m *MoneyTransferMetadata) CopyTo(dst *MoneyTransferMetadata) { - dst.Source = m.Source - dst.Destination = m.Destination - dst.Quantity = m.Quantity - dst.ExchangeCurrency = m.ExchangeCurrency - dst.ExchangeRate = m.ExchangeRate - dst.UsdMarketValue = m.UsdMarketValue - dst.IsWithdrawal = m.IsWithdrawal -} - -func (m *MoneyTransferMetadata) Validate() error { - if len(m.Source) == 0 { - return errors.New("payment source is required") - } - - if len(m.Destination) == 0 { - return errors.New("payment destination is required") - } - - if m.Quantity == 0 { - return errors.New("payment quantity cannot be zero") - } - - if len(m.ExchangeCurrency) == 0 { - return errors.New("payment exchange currency is required") - } - - if m.ExchangeRate == 0 { - return errors.New("payment exchange rate cannot be zero") - } - - if m.UsdMarketValue == 0 { - return errors.New("payment usd market value cannot be zero") - } - - return nil -} - -func (m *AccountManagementMetadata) Clone() AccountManagementMetadata { - return AccountManagementMetadata{ - TokenAccount: m.TokenAccount, - } -} - -func (m *AccountManagementMetadata) CopyTo(dst *AccountManagementMetadata) { - dst.TokenAccount = m.TokenAccount -} - -func (m *AccountManagementMetadata) Validate() error { - if len(m.TokenAccount) == 0 { - return errors.New("created token account is required") - } - return nil } @@ -559,188 +246,6 @@ func (m *OpenAccountsMetadata) Validate() error { return nil } -func (m *SendPrivatePaymentMetadata) Clone() SendPrivatePaymentMetadata { - var tipMetadata *TipMetadata - if m.TipMetadata != nil { - tipMetadata = &TipMetadata{ - Platform: m.TipMetadata.Platform, - Username: m.TipMetadata.Username, - } - } - - return SendPrivatePaymentMetadata{ - DestinationOwnerAccount: m.DestinationOwnerAccount, - DestinationTokenAccount: m.DestinationTokenAccount, - Quantity: m.Quantity, - - ExchangeCurrency: m.ExchangeCurrency, - ExchangeRate: m.ExchangeRate, - NativeAmount: m.NativeAmount, - UsdMarketValue: m.UsdMarketValue, - - IsWithdrawal: m.IsWithdrawal, - IsRemoteSend: m.IsRemoteSend, - IsMicroPayment: m.IsMicroPayment, - IsTip: m.IsTip, - IsChat: m.IsChat, - - TipMetadata: tipMetadata, - ChatId: m.ChatId, - } -} - -func (m *SendPrivatePaymentMetadata) CopyTo(dst *SendPrivatePaymentMetadata) { - var tipMetadata *TipMetadata - if m.TipMetadata != nil { - tipMetadata = &TipMetadata{ - Platform: m.TipMetadata.Platform, - Username: m.TipMetadata.Username, - } - } - - dst.DestinationOwnerAccount = m.DestinationOwnerAccount - dst.DestinationTokenAccount = m.DestinationTokenAccount - dst.Quantity = m.Quantity - - dst.ExchangeCurrency = m.ExchangeCurrency - dst.ExchangeRate = m.ExchangeRate - dst.NativeAmount = m.NativeAmount - dst.UsdMarketValue = m.UsdMarketValue - - dst.IsWithdrawal = m.IsWithdrawal - dst.IsRemoteSend = m.IsRemoteSend - dst.IsMicroPayment = m.IsMicroPayment - dst.IsTip = m.IsTip - dst.IsChat = m.IsChat - - dst.TipMetadata = tipMetadata - dst.ChatId = m.ChatId -} - -func (m *SendPrivatePaymentMetadata) Validate() error { - if len(m.DestinationTokenAccount) == 0 { - return errors.New("destination token account is required") - } - - if m.Quantity == 0 { - return errors.New("quantity cannot be zero") - } - - if len(m.ExchangeCurrency) == 0 { - return errors.New("exchange currency is required") - } - - if m.ExchangeRate == 0 { - return errors.New("exchange rate cannot be zero") - } - - if m.NativeAmount == 0 { - return errors.New("native amount cannot be zero") - } - - if m.UsdMarketValue == 0 { - return errors.New("usd market value cannot be zero") - } - - if m.IsTip { - if m.TipMetadata == nil { - return errors.New("tip metadata required for tips") - } - - if m.TipMetadata.Platform == transactionpb.TippedUser_UNKNOWN { - return errors.New("tip platform is required") - } - - if len(m.TipMetadata.Username) == 0 { - return errors.New("tip username is required") - } - } else if m.TipMetadata != nil { - return errors.New("tip metadata can only be set for tips") - } - - if m.IsChat { - if len(m.ChatId) == 0 { - return errors.New("chat_id required for chat") - } - } else if m.ChatId != "" { - return errors.New("chat_id can only be set for chats") - } - - return nil -} - -func (m *ReceivePaymentsPrivatelyMetadata) Clone() ReceivePaymentsPrivatelyMetadata { - return ReceivePaymentsPrivatelyMetadata{ - Source: m.Source, - Quantity: m.Quantity, - IsDeposit: m.IsDeposit, - - UsdMarketValue: m.UsdMarketValue, - } -} - -func (m *ReceivePaymentsPrivatelyMetadata) CopyTo(dst *ReceivePaymentsPrivatelyMetadata) { - dst.Source = m.Source - dst.Quantity = m.Quantity - dst.IsDeposit = m.IsDeposit - - dst.UsdMarketValue = m.UsdMarketValue -} - -func (m *ReceivePaymentsPrivatelyMetadata) Validate() error { - if len(m.Source) == 0 { - return errors.New("source is required") - } - - if m.Quantity == 0 { - return errors.New("quantity cannot be zero") - } - - if m.UsdMarketValue == 0 { - return errors.New("usd market value cannot be zero") - } - - return nil -} - -func (m *SaveRecentRootMetadata) Clone() SaveRecentRootMetadata { - return SaveRecentRootMetadata{ - TreasuryPool: m.TreasuryPool, - PreviousMostRecentRoot: m.PreviousMostRecentRoot, - } -} - -func (m *SaveRecentRootMetadata) CopyTo(dst *SaveRecentRootMetadata) { - dst.TreasuryPool = m.TreasuryPool - dst.PreviousMostRecentRoot = m.PreviousMostRecentRoot -} - -func (m *SaveRecentRootMetadata) Validate() error { - if len(m.TreasuryPool) == 0 { - return errors.New("treasury pool is required") - } - - if len(m.PreviousMostRecentRoot) == 0 { - return errors.New("previous most recent root is required") - } - - return nil -} - -func (m *MigrateToPrivacy2022Metadata) Clone() MigrateToPrivacy2022Metadata { - return MigrateToPrivacy2022Metadata{ - Quantity: m.Quantity, - } -} - -func (m *MigrateToPrivacy2022Metadata) CopyTo(dst *MigrateToPrivacy2022Metadata) { - dst.Quantity = m.Quantity -} - -func (m *MigrateToPrivacy2022Metadata) Validate() error { - return nil -} - func (m *ExternalDepositMetadata) Clone() ExternalDepositMetadata { return ExternalDepositMetadata{ DestinationOwnerAccount: m.DestinationOwnerAccount, @@ -889,48 +394,6 @@ func (m *ReceivePaymentsPubliclyMetadata) Validate() error { return nil } -func (m *EstablishRelationshipMetadata) Clone() EstablishRelationshipMetadata { - return EstablishRelationshipMetadata{ - RelationshipTo: m.RelationshipTo, - } -} - -func (m *EstablishRelationshipMetadata) CopyTo(dst *EstablishRelationshipMetadata) { - dst.RelationshipTo = m.RelationshipTo -} - -func (m *EstablishRelationshipMetadata) Validate() error { - if len(m.RelationshipTo) == 0 { - return errors.New("relationship is required") - } - - return nil -} - -func (m *LoginMetadata) Clone() LoginMetadata { - return LoginMetadata{ - App: m.App, - UserId: m.UserId, - } -} - -func (m *LoginMetadata) CopyTo(dst *LoginMetadata) { - dst.App = m.App - dst.UserId = m.UserId -} - -func (m *LoginMetadata) Validate() error { - if len(m.App) == 0 { - return errors.New("app is required") - } - - if len(m.UserId) == 0 { - return errors.New("user is required") - } - - return nil -} - func (s State) IsTerminal() bool { switch s { case StateConfirmed: diff --git a/pkg/code/data/intent/memory/store.go b/pkg/code/data/intent/memory/store.go index 94293dbd..23a38e2f 100644 --- a/pkg/code/data/intent/memory/store.go +++ b/pkg/code/data/intent/memory/store.go @@ -2,11 +2,10 @@ package memory import ( "context" - "sort" + "errors" "sync" "time" - "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/code/data/intent" ) @@ -74,45 +73,10 @@ func (s *store) findByState(state intent.State) []*intent.Record { return res } -func (s *store) findByOwner(owner string) []*intent.Record { - res := make([]*intent.Record, 0) - for _, item := range s.records { - if item.InitiatorOwnerAccount == owner { - res = append(res, item) - continue - } - - if item.SendPrivatePaymentMetadata != nil && item.SendPrivatePaymentMetadata.DestinationOwnerAccount == owner { - res = append(res, item) - continue - } - - if item.SendPublicPaymentMetadata != nil && item.SendPublicPaymentMetadata.DestinationOwnerAccount == owner { - res = append(res, item) - continue - } - - if item.ExternalDepositMetadata != nil && item.ExternalDepositMetadata.DestinationOwnerAccount == owner { - res = append(res, item) - continue - } - } - - return res -} - func (s *store) findByDestination(destination string) []*intent.Record { res := make([]*intent.Record, 0) for _, item := range s.records { switch item.IntentType { - case intent.LegacyPayment: - if item.MoneyTransferMetadata.Destination == destination { - res = append(res, item) - } - case intent.SendPrivatePayment: - if item.SendPrivatePaymentMetadata.DestinationTokenAccount == destination { - res = append(res, item) - } case intent.ExternalDeposit: if item.ExternalDepositMetadata.DestinationTokenAccount == destination { res = append(res, item) @@ -130,14 +94,6 @@ func (s *store) findBySource(source string) []*intent.Record { res := make([]*intent.Record, 0) for _, item := range s.records { switch item.IntentType { - case intent.LegacyPayment: - if item.MoneyTransferMetadata.Source == source { - res = append(res, item) - } - case intent.ReceivePaymentsPrivately: - if item.ReceivePaymentsPrivatelyMetadata.Source == source { - res = append(res, item) - } case intent.ReceivePaymentsPublicly: if item.ReceivePaymentsPubliclyMetadata.Source == source { res = append(res, item) @@ -147,78 +103,6 @@ func (s *store) findBySource(source string) []*intent.Record { return res } -func (s *store) findByInitiatorPhoneNumberSinceTimestamp(phoneNumber string, since time.Time) []*intent.Record { - res := make([]*intent.Record, 0) - for _, item := range s.records { - if item.CreatedAt.Before(since) { - continue - } - - if item.InitiatorPhoneNumber != nil && *item.InitiatorPhoneNumber == phoneNumber { - res = append(res, item) - } - } - return res -} - -func (s *store) findForAntispam(intentType intent.Type, phoneNumber string, states []intent.State, since time.Time) []*intent.Record { - res := make([]*intent.Record, 0) - for _, item := range s.records { - if item.IntentType != intentType { - continue - } - - if item.InitiatorPhoneNumber == nil || *item.InitiatorPhoneNumber != phoneNumber { - continue - } - - if item.CreatedAt.Before(since) { - continue - } - - for _, state := range states { - if item.State == state { - res = append(res, item) - } - } - } - return res -} - -func (s *store) findOwnerInteractionsForAntispam(sourceOwner, destinationOwner string, states []intent.State, since time.Time) []*intent.Record { - res := make([]*intent.Record, 0) - for _, item := range s.records { - if item.InitiatorOwnerAccount != sourceOwner { - continue - } - - var destinationOwnerToCheck string - switch item.IntentType { - case intent.SendPrivatePayment: - destinationOwnerToCheck = item.SendPrivatePaymentMetadata.DestinationOwnerAccount - case intent.SendPublicPayment: - destinationOwnerToCheck = item.SendPublicPaymentMetadata.DestinationOwnerAccount - default: - continue - } - - if destinationOwnerToCheck != destinationOwner { - continue - } - - if item.CreatedAt.Before(since) { - continue - } - - for _, state := range states { - if item.State == state { - res = append(res, item) - } - } - } - return res -} - func (s *store) findByInitiatorAndType(intentType intent.Type, owner string) []*intent.Record { res := make([]*intent.Record, 0) for _, item := range s.records { @@ -235,86 +119,6 @@ func (s *store) findByInitiatorAndType(intentType intent.Type, owner string) []* return res } -func (s *store) findPrePrivacy2022ByTokenAddress(address string) []*intent.Record { - res := make([]*intent.Record, 0) - for _, item := range s.records { - if item.MoneyTransferMetadata != nil { - if item.MoneyTransferMetadata.Source == address { - res = append(res, item) - continue - } - if item.MoneyTransferMetadata.Destination == address { - res = append(res, item) - continue - } - } - - if item.AccountManagementMetadata != nil && item.AccountManagementMetadata.TokenAccount == address { - res = append(res, item) - continue - } - } - return res -} - -func sumQuarkAmount(items []*intent.Record) uint64 { - var value uint64 - for _, item := range items { - if item.SendPrivatePaymentMetadata != nil { - value += item.SendPrivatePaymentMetadata.Quantity - } - if item.ReceivePaymentsPrivatelyMetadata != nil { - value += item.ReceivePaymentsPrivatelyMetadata.Quantity - } - } - return value -} - -func sumUsdMarketValue(items []*intent.Record) float64 { - var value float64 - for _, item := range items { - if item.SendPrivatePaymentMetadata != nil { - value += item.SendPrivatePaymentMetadata.UsdMarketValue - } - if item.ReceivePaymentsPrivatelyMetadata != nil { - value += item.ReceivePaymentsPrivatelyMetadata.UsdMarketValue - } - } - return value -} - -func (s *store) filter(items []*intent.Record, cursor query.Cursor, limit uint64, direction query.Ordering) []*intent.Record { - var start uint64 - - start = 0 - if direction == query.Descending { - start = s.last + 1 - } - if len(cursor) > 0 { - start = cursor.ToUint64() - } - - var res []*intent.Record - for _, item := range items { - if item.Id > start && direction == query.Ascending { - res = append(res, item) - } - if item.Id < start && direction == query.Descending { - res = append(res, item) - } - } - - if direction == query.Descending { - sort.Sort(sort.Reverse(ById(res))) - } - - if len(res) >= int(limit) { - return res[:limit] - } - - return res -} - func (s *store) filterByState(items []*intent.Record, include bool, states ...intent.State) []*intent.Record { var res []*intent.Record @@ -343,32 +147,10 @@ func (s *store) filterByType(items []*intent.Record, intentType intent.Type) []* return res } -func (s *store) filterByDeposit(items []*intent.Record) []*intent.Record { - var res []*intent.Record - - for _, item := range items { - if item.IntentType != intent.ReceivePaymentsPrivately { - continue - } - - if !item.ReceivePaymentsPrivatelyMetadata.IsDeposit { - continue - } - - res = append(res, item) - } - - return res -} - func (s *store) filterByRemoteSendFlag(items []*intent.Record, want bool) []*intent.Record { var res []*intent.Record for _, item := range items { switch item.IntentType { - case intent.SendPrivatePayment: - if item.SendPrivatePaymentMetadata.IsRemoteSend == want { - res = append(res, item) - } case intent.ReceivePaymentsPublicly: if item.ReceivePaymentsPubliclyMetadata.IsRemoteSend == want { res = append(res, item) @@ -434,151 +216,52 @@ func (s *store) GetLatestByInitiatorAndType(ctx context.Context, intentType inte return latest, nil } -func (s *store) GetAllByOwner(ctx context.Context, owner string, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*intent.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if items := s.findByOwner(owner); len(items) > 0 { - res := s.filter(items, cursor, limit, direction) - - if len(res) == 0 { - return nil, intent.ErrIntentNotFound - } - - return res, nil - } - - return nil, intent.ErrIntentNotFound -} - -func (s *store) CountForAntispam(ctx context.Context, intentType intent.Type, phoneNumber string, states []intent.State, since time.Time) (uint64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findForAntispam(intentType, phoneNumber, states, since) - return uint64(len(items)), nil -} - -func (s *store) CountOwnerInteractionsForAntispam(ctx context.Context, sourceOwner, destinationOwner string, states []intent.State, since time.Time) (uint64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findOwnerInteractionsForAntispam(sourceOwner, destinationOwner, states, since) - return uint64(len(items)), nil -} - -func (s *store) GetTransactedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByInitiatorPhoneNumberSinceTimestamp(phoneNumber, since) - items = s.filterByState(items, false, intent.StateRevoked) - items = s.filterByType(items, intent.SendPrivatePayment) - return sumQuarkAmount(items), sumUsdMarketValue(items), nil -} - -func (s *store) GetDepositedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByInitiatorPhoneNumberSinceTimestamp(phoneNumber, since) - items = s.filterByState(items, false, intent.StateRevoked) - items = s.filterByDeposit(items) - return sumQuarkAmount(items), sumUsdMarketValue(items), nil -} - -func (s *store) GetNetBalanceFromPrePrivacy2022Intents(ctx context.Context, account string) (int64, error) { - s.mu.Lock() - defer s.mu.Unlock() +func (s *store) GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { + return nil, errors.New("not implemented") - var res int64 + /* + s.mu.Lock() + defer s.mu.Unlock() - items := s.findPrePrivacy2022ByTokenAddress(account) - for _, item := range items { - if item.IntentType != intent.LegacyPayment { - continue - } + items := s.findByDestination(giftCardVault) + items = s.filterByType(items, intent.SendPrivatePayment) + items = s.filterByState(items, false, intent.StateRevoked) + items = s.filterByRemoteSendFlag(items, true) - if item.State == intent.StateUnknown || item.State == intent.StateRevoked { - continue + if len(items) == 0 { + return nil, intent.ErrIntentNotFound } - if item.MoneyTransferMetadata.Source == account { - res -= int64(item.MoneyTransferMetadata.Quantity) + if len(items) > 1 { + return nil, intent.ErrMultilpeIntentsFound } - if item.MoneyTransferMetadata.Destination == account { - res += int64(item.MoneyTransferMetadata.Quantity) - } - } - - return res, nil + cloned := items[0].Clone() + return &cloned, nil + */ } -func (s *store) GetLatestSaveRecentRootIntentForTreasury(ctx context.Context, treasury string) (*intent.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() +func (s *store) GetGiftCardClaimedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { + return nil, errors.New("not implemented") - var latest *intent.Record - for _, record := range s.records { - if record.IntentType != intent.SaveRecentRoot { - continue - } + /* + s.mu.Lock() + defer s.mu.Unlock() - if record.SaveRecentRootMetadata.TreasuryPool != treasury { - continue - } + items := s.findBySource(giftCardVault) + items = s.filterByType(items, intent.ReceivePaymentsPublicly) + items = s.filterByState(items, false, intent.StateRevoked) + items = s.filterByRemoteSendFlag(items, true) - if latest == nil || latest.Id < record.Id { - latest = record + if len(items) == 0 { + return nil, intent.ErrIntentNotFound } - } - if latest == nil { - return nil, intent.ErrIntentNotFound - } - cloned := latest.Clone() - return &cloned, nil -} - -func (s *store) GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByDestination(giftCardVault) - items = s.filterByType(items, intent.SendPrivatePayment) - items = s.filterByState(items, false, intent.StateRevoked) - items = s.filterByRemoteSendFlag(items, true) - - if len(items) == 0 { - return nil, intent.ErrIntentNotFound - } - - if len(items) > 1 { - return nil, intent.ErrMultilpeIntentsFound - } - - cloned := items[0].Clone() - return &cloned, nil -} - -func (s *store) GetGiftCardClaimedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findBySource(giftCardVault) - items = s.filterByType(items, intent.ReceivePaymentsPublicly) - items = s.filterByState(items, false, intent.StateRevoked) - items = s.filterByRemoteSendFlag(items, true) - - if len(items) == 0 { - return nil, intent.ErrIntentNotFound - } - - if len(items) > 1 { - return nil, intent.ErrMultilpeIntentsFound - } + if len(items) > 1 { + return nil, intent.ErrMultilpeIntentsFound + } - cloned := items[0].Clone() - return &cloned, nil + cloned := items[0].Clone() + return &cloned, nil + */ } diff --git a/pkg/code/data/intent/postgres/model.go b/pkg/code/data/intent/postgres/model.go index 4aa62649..554461e6 100644 --- a/pkg/code/data/intent/postgres/model.go +++ b/pkg/code/data/intent/postgres/model.go @@ -4,20 +4,15 @@ import ( "context" "database/sql" "errors" - "fmt" - "strconv" "strings" "time" "github.com/jmoiron/sqlx" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/currency" pgutil "github.com/code-payments/code-server/pkg/database/postgres" - q "github.com/code-payments/code-server/pkg/database/query" ) const ( @@ -26,35 +21,27 @@ const ( ) type intentModel struct { - Id sql.NullInt64 `db:"id"` - IntentId string `db:"intent_id"` - IntentType uint `db:"intent_type"` - InitiatorOwner string `db:"owner"` // todo: rename the DB field to initiator_owner - Source string `db:"source"` - DestinationOwnerAccount string `db:"destination_owner"` - DestinationTokenAccount string `db:"destination"` // todo: rename the DB field to be destination_token - Quantity uint64 `db:"quantity"` - TreasuryPool sql.NullString `db:"treasury_pool"` - RecentRoot sql.NullString `db:"recent_root"` - ExchangeCurrency string `db:"exchange_currency"` - ExchangeRate float64 `db:"exchange_rate"` - NativeAmount float64 `db:"native_amount"` - UsdMarketValue float64 `db:"usd_market_value"` - IsWithdrawal bool `db:"is_withdraw"` - IsDeposit bool `db:"is_deposit"` - IsRemoteSend bool `db:"is_remote_send"` - IsReturned bool `db:"is_returned"` - IsIssuerVoidingGiftCard bool `db:"is_issuer_voiding_gift_card"` - IsMicroPayment bool `db:"is_micro_payment"` - IsTip bool `db:"is_tip"` - TipPlatform sql.NullInt16 `db:"tip_platform"` - TippedUsername sql.NullString `db:"tipped_username"` - IsChat bool `db:"is_chat"` - ChatId sql.NullString `db:"chat_id"` - RelationshipTo sql.NullString `db:"relationship_to"` - InitiatorPhoneNumber sql.NullString `db:"phone_number"` // todo: rename the DB field to initiator_phone_number - State uint `db:"state"` - CreatedAt time.Time `db:"created_at"` + Id sql.NullInt64 `db:"id"` + IntentId string `db:"intent_id"` + IntentType uint `db:"intent_type"` + InitiatorOwner string `db:"owner"` // todo: rename the DB field to initiator_owner + Source string `db:"source"` + DestinationOwnerAccount string `db:"destination_owner"` + DestinationTokenAccount string `db:"destination"` // todo: rename the DB field to be destination_token + Quantity uint64 `db:"quantity"` + ExchangeCurrency string `db:"exchange_currency"` + ExchangeRate float64 `db:"exchange_rate"` + NativeAmount float64 `db:"native_amount"` + UsdMarketValue float64 `db:"usd_market_value"` + IsWithdrawal bool `db:"is_withdraw"` + IsDeposit bool `db:"is_deposit"` + IsRemoteSend bool `db:"is_remote_send"` + IsReturned bool `db:"is_returned"` + IsIssuerVoidingGiftCard bool `db:"is_issuer_voiding_gift_card"` + IsMicroPayment bool `db:"is_micro_payment"` + ExtendedMetadata []byte `db:"extended_metadata"` + State uint `db:"state"` + CreatedAt time.Time `db:"created_at"` } func toIntentModel(obj *intent.Record) (*intentModel, error) { @@ -67,79 +54,17 @@ func toIntentModel(obj *intent.Record) (*intentModel, error) { } m := &intentModel{ - Id: sql.NullInt64{Int64: int64(obj.Id), Valid: true}, - IntentId: obj.IntentId, - IntentType: uint(obj.IntentType), - InitiatorOwner: obj.InitiatorOwnerAccount, - State: uint(obj.State), - CreatedAt: obj.CreatedAt, - } - - if obj.InitiatorPhoneNumber != nil { - m.InitiatorPhoneNumber.Valid = true - m.InitiatorPhoneNumber.String = *obj.InitiatorPhoneNumber + Id: sql.NullInt64{Int64: int64(obj.Id), Valid: true}, + IntentId: obj.IntentId, + IntentType: uint(obj.IntentType), + InitiatorOwner: obj.InitiatorOwnerAccount, + ExtendedMetadata: obj.ExtendedMetadata, + State: uint(obj.State), + CreatedAt: obj.CreatedAt, } switch obj.IntentType { - case intent.LegacyPayment: - m.Source = obj.MoneyTransferMetadata.Source - m.DestinationTokenAccount = obj.MoneyTransferMetadata.Destination - m.Quantity = obj.MoneyTransferMetadata.Quantity - - m.ExchangeCurrency = strings.ToLower(string(obj.MoneyTransferMetadata.ExchangeCurrency)) - m.ExchangeRate = obj.MoneyTransferMetadata.ExchangeRate - m.UsdMarketValue = obj.MoneyTransferMetadata.UsdMarketValue - - m.IsWithdrawal = obj.MoneyTransferMetadata.IsWithdrawal - case intent.LegacyCreateAccount: - m.Source = obj.AccountManagementMetadata.TokenAccount case intent.OpenAccounts: - case intent.SendPrivatePayment: - m.DestinationOwnerAccount = obj.SendPrivatePaymentMetadata.DestinationOwnerAccount - m.DestinationTokenAccount = obj.SendPrivatePaymentMetadata.DestinationTokenAccount - m.Quantity = obj.SendPrivatePaymentMetadata.Quantity - - m.ExchangeCurrency = strings.ToLower(string(obj.SendPrivatePaymentMetadata.ExchangeCurrency)) - m.ExchangeRate = obj.SendPrivatePaymentMetadata.ExchangeRate - m.NativeAmount = obj.SendPrivatePaymentMetadata.NativeAmount - m.UsdMarketValue = obj.SendPrivatePaymentMetadata.UsdMarketValue - - m.IsWithdrawal = obj.SendPrivatePaymentMetadata.IsWithdrawal - m.IsRemoteSend = obj.SendPrivatePaymentMetadata.IsRemoteSend - m.IsMicroPayment = obj.SendPrivatePaymentMetadata.IsMicroPayment - m.IsTip = obj.SendPrivatePaymentMetadata.IsTip - m.IsChat = obj.SendPrivatePaymentMetadata.IsChat - - if m.IsTip { - m.TipPlatform = sql.NullInt16{ - Valid: true, - Int16: int16(obj.SendPrivatePaymentMetadata.TipMetadata.Platform), - } - m.TippedUsername = sql.NullString{ - Valid: true, - String: obj.SendPrivatePaymentMetadata.TipMetadata.Username, - } - } - - if m.IsChat { - m.ChatId = sql.NullString{ - Valid: true, - String: obj.SendPrivatePaymentMetadata.ChatId, - } - } - case intent.ReceivePaymentsPrivately: - m.Source = obj.ReceivePaymentsPrivatelyMetadata.Source - m.Quantity = obj.ReceivePaymentsPrivatelyMetadata.Quantity - m.IsDeposit = obj.ReceivePaymentsPrivatelyMetadata.IsDeposit - - m.UsdMarketValue = obj.ReceivePaymentsPrivatelyMetadata.UsdMarketValue - case intent.SaveRecentRoot: - m.TreasuryPool.Valid = true - m.TreasuryPool.String = obj.SaveRecentRootMetadata.TreasuryPool - m.RecentRoot.Valid = true - m.RecentRoot.String = obj.SaveRecentRootMetadata.PreviousMostRecentRoot - case intent.MigrateToPrivacy2022: - m.Quantity = obj.MigrateToPrivacy2022Metadata.Quantity case intent.ExternalDeposit: m.DestinationOwnerAccount = obj.ExternalDepositMetadata.DestinationOwnerAccount m.DestinationTokenAccount = obj.ExternalDepositMetadata.DestinationTokenAccount @@ -168,17 +93,6 @@ func toIntentModel(obj *intent.Record) (*intentModel, error) { m.NativeAmount = obj.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount m.UsdMarketValue = obj.ReceivePaymentsPubliclyMetadata.UsdMarketValue - case intent.EstablishRelationship: - m.RelationshipTo = sql.NullString{ - Valid: true, - String: obj.EstablishRelationshipMetadata.RelationshipTo, - } - case intent.Login: - m.RelationshipTo = sql.NullString{ - Valid: true, - String: obj.LoginMetadata.App, - } - m.Source = obj.LoginMetadata.UserId default: return nil, errors.New("unsupported intent type") } @@ -192,79 +106,14 @@ func fromIntentModel(obj *intentModel) *intent.Record { IntentId: obj.IntentId, IntentType: intent.Type(obj.IntentType), InitiatorOwnerAccount: obj.InitiatorOwner, + ExtendedMetadata: obj.ExtendedMetadata, State: intent.State(obj.State), CreatedAt: obj.CreatedAt.UTC(), } - if obj.InitiatorPhoneNumber.Valid { - record.InitiatorPhoneNumber = &obj.InitiatorPhoneNumber.String - } - switch record.IntentType { - case intent.LegacyPayment: - record.MoneyTransferMetadata = &intent.MoneyTransferMetadata{ - Source: obj.Source, - Destination: obj.DestinationTokenAccount, - Quantity: obj.Quantity, - - ExchangeCurrency: currency.Code(obj.ExchangeCurrency), - ExchangeRate: obj.ExchangeRate, - UsdMarketValue: obj.UsdMarketValue, - - IsWithdrawal: obj.IsWithdrawal, - } - case intent.LegacyCreateAccount: - record.AccountManagementMetadata = &intent.AccountManagementMetadata{ - TokenAccount: obj.Source, - } case intent.OpenAccounts: record.OpenAccountsMetadata = &intent.OpenAccountsMetadata{} - case intent.SendPrivatePayment: - record.SendPrivatePaymentMetadata = &intent.SendPrivatePaymentMetadata{ - DestinationOwnerAccount: obj.DestinationOwnerAccount, - DestinationTokenAccount: obj.DestinationTokenAccount, - Quantity: obj.Quantity, - - ExchangeCurrency: currency.Code(obj.ExchangeCurrency), - ExchangeRate: obj.ExchangeRate, - NativeAmount: obj.NativeAmount, - UsdMarketValue: obj.UsdMarketValue, - - IsWithdrawal: obj.IsWithdrawal, - IsRemoteSend: obj.IsRemoteSend, - IsMicroPayment: obj.IsMicroPayment, - IsTip: obj.IsTip, - IsChat: obj.IsChat, - } - - if record.SendPrivatePaymentMetadata.IsTip { - record.SendPrivatePaymentMetadata.TipMetadata = &intent.TipMetadata{ - Platform: transactionpb.TippedUser_Platform(obj.TipPlatform.Int16), - Username: obj.TippedUsername.String, - } - } - - if record.SendPrivatePaymentMetadata.IsChat { - record.SendPrivatePaymentMetadata.ChatId = obj.ChatId.String - } - - case intent.ReceivePaymentsPrivately: - record.ReceivePaymentsPrivatelyMetadata = &intent.ReceivePaymentsPrivatelyMetadata{ - Source: obj.Source, - Quantity: obj.Quantity, - IsDeposit: obj.IsDeposit, - - UsdMarketValue: obj.UsdMarketValue, - } - case intent.SaveRecentRoot: - record.SaveRecentRootMetadata = &intent.SaveRecentRootMetadata{ - TreasuryPool: obj.TreasuryPool.String, - PreviousMostRecentRoot: obj.RecentRoot.String, - } - case intent.MigrateToPrivacy2022: - record.MigrateToPrivacy2022Metadata = &intent.MigrateToPrivacy2022Metadata{ - Quantity: obj.Quantity, - } case intent.ExternalDeposit: record.ExternalDepositMetadata = &intent.ExternalDepositMetadata{ DestinationOwnerAccount: obj.DestinationOwnerAccount, @@ -299,15 +148,6 @@ func fromIntentModel(obj *intentModel) *intent.Record { UsdMarketValue: obj.UsdMarketValue, } - case intent.EstablishRelationship: - record.EstablishRelationshipMetadata = &intent.EstablishRelationshipMetadata{ - RelationshipTo: obj.RelationshipTo.String, - } - case intent.Login: - record.LoginMetadata = &intent.LoginMetadata{ - App: obj.RelationshipTo.String, - UserId: obj.Source, - } } return record @@ -316,16 +156,16 @@ func fromIntentModel(obj *intentModel) *intent.Record { func (m *intentModel) dbSave(ctx context.Context, db *sqlx.DB) error { return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { query := `INSERT INTO ` + intentTableName + ` - (intent_id, intent_type, owner, source, destination_owner, destination, quantity, treasury_pool, recent_root, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, is_tip, is_chat, relationship_to, tip_platform, tipped_username, chat_id, phone_number, state, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28) + (intent_id, intent_type, owner, source, destination_owner, destination, quantity, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, extended_metadata, state, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) ON CONFLICT (intent_id) DO UPDATE - SET state = $27 + SET state = $19 WHERE ` + intentTableName + `.intent_id = $1 RETURNING - id, intent_id, intent_type, source, destination_owner, destination, quantity, treasury_pool, recent_root, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, is_tip, is_chat, relationship_to, tip_platform, tipped_username, chat_id, phone_number, state, created_at` + id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, extended_metadata, state, created_at` err := tx.QueryRowxContext( ctx, @@ -337,8 +177,6 @@ func (m *intentModel) dbSave(ctx context.Context, db *sqlx.DB) error { m.DestinationOwnerAccount, m.DestinationTokenAccount, m.Quantity, - m.TreasuryPool, - m.RecentRoot, m.ExchangeCurrency, m.ExchangeRate, m.NativeAmount, @@ -349,13 +187,7 @@ func (m *intentModel) dbSave(ctx context.Context, db *sqlx.DB) error { m.IsReturned, m.IsIssuerVoidingGiftCard, m.IsMicroPayment, - m.IsTip, - m.IsChat, - m.RelationshipTo, - m.TipPlatform, - m.TippedUsername, - m.ChatId, - m.InitiatorPhoneNumber, + m.ExtendedMetadata, m.State, m.CreatedAt, ).StructScan(m) @@ -367,7 +199,7 @@ func (m *intentModel) dbSave(ctx context.Context, db *sqlx.DB) error { func dbGetIntent(ctx context.Context, db *sqlx.DB, intentID string) (*intentModel, error) { res := &intentModel{} - query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, treasury_pool, recent_root, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, is_tip, is_chat, relationship_to, tip_platform, tipped_username, chat_id, phone_number, state, created_at + query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, extended_metadata, state, created_at FROM ` + intentTableName + ` WHERE intent_id = $1 LIMIT 1` @@ -382,7 +214,7 @@ func dbGetIntent(ctx context.Context, db *sqlx.DB, intentID string) (*intentMode func dbGetLatestByInitiatorAndType(ctx context.Context, db *sqlx.DB, intentType intent.Type, owner string) (*intentModel, error) { res := &intentModel{} - query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, treasury_pool, recent_root, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, is_tip, is_chat, relationship_to, tip_platform, tipped_username, chat_id, phone_number, state, created_at + query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, extended_metadata, state, created_at FROM ` + intentTableName + ` WHERE owner = $1 AND intent_type = $2 ORDER BY created_at DESC @@ -395,189 +227,10 @@ func dbGetLatestByInitiatorAndType(ctx context.Context, db *sqlx.DB, intentType return res, nil } -// todo: fix legacy intents -func dbGetAllByOwner(ctx context.Context, db *sqlx.DB, owner string, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*intentModel, error) { - res := []*intentModel{} - - query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, treasury_pool, recent_root, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, is_tip, is_chat, relationship_to, tip_platform, tipped_username, chat_id, phone_number, state, created_at - FROM ` + intentTableName + ` - WHERE (owner = $1 OR destination_owner = $1) AND (intent_type != $2 AND intent_type != $3) - ` - - opts := []interface{}{owner, intent.LegacyPayment, intent.LegacyCreateAccount} - query, opts = q.PaginateQuery(query, opts, cursor, limit, direction) - - err := db.SelectContext(ctx, &res, query, opts...) - if err != nil { - return nil, pgutil.CheckNoRows(err, intent.ErrIntentNotFound) - } - - if len(res) == 0 { - return nil, intent.ErrIntentNotFound - } - - return res, nil -} - -func dbGetCountForAntispam(ctx context.Context, db *sqlx.DB, intentType intent.Type, phoneNumber string, states []intent.State, since time.Time) (uint64, error) { - var res uint64 - - // Ugh, the Go SQL implementation has a really hard time with arrays... - statesAsString := make([]string, len(states)) - for i, state := range states { - statesAsString[i] = strconv.Itoa(int(state)) - } - - query := fmt.Sprintf( - `SELECT COUNT(*) FROM `+intentTableName+` WHERE phone_number = $1 AND created_at >= $2 AND intent_type = $3 AND state IN (%s)`, - strings.Join(statesAsString, ","), - ) - err := db.GetContext( - ctx, - &res, - query, - phoneNumber, - since, - intentType, - ) - if err != nil { - return 0, err - } - - return res, nil -} - -func dbGetCountOwnerInteractionsForAntispam(ctx context.Context, db *sqlx.DB, sourceOwner, destinationOwner string, states []intent.State, since time.Time) (uint64, error) { - var res uint64 - - // Ugh, the Go SQL implementation has a really hard time with arrays... - statesAsString := make([]string, len(states)) - for i, state := range states { - statesAsString[i] = strconv.Itoa(int(state)) - } - - query := fmt.Sprintf( - `SELECT COUNT(*) FROM `+intentTableName+` WHERE owner = $1 AND destination_owner = $2 AND created_at >= $3 AND state IN (%s)`, - strings.Join(statesAsString, ","), - ) - err := db.GetContext( - ctx, - &res, - query, - sourceOwner, - destinationOwner, - since, - ) - if err != nil { - return 0, err - } - - return res, nil -} - -func dbGetTransactedAmountForAntiMoneyLaundering(ctx context.Context, db *sqlx.DB, phoneNumber string, since time.Time) (uint64, float64, error) { - res := struct { - TotalQuarkValue sql.NullInt64 `db:"total_quark_value"` - TotalUsdMarketValue sql.NullFloat64 `db:"total_usd_value"` - }{} - - query := `SELECT SUM(quantity) AS total_quark_value, SUM(usd_market_value) AS total_usd_value FROM ` + intentTableName + ` - WHERE phone_number = $1 AND created_at >= $2 AND intent_type = $3 AND state != $4 - ` - err := db.GetContext( - ctx, - &res, - query, - phoneNumber, - since, - intent.SendPrivatePayment, - intent.StateRevoked, - ) - if err != nil { - return 0, 0, err - } - - if !res.TotalQuarkValue.Valid || !res.TotalUsdMarketValue.Valid { - return 0, 0, nil - } - return uint64(res.TotalQuarkValue.Int64), res.TotalUsdMarketValue.Float64, nil -} - -func dbGetDepositedAmountForAntiMoneyLaundering(ctx context.Context, db *sqlx.DB, phoneNumber string, since time.Time) (uint64, float64, error) { - res := struct { - TotalQuarkValue sql.NullInt64 `db:"total_quark_value"` - TotalUsdMarketValue sql.NullFloat64 `db:"total_usd_value"` - }{} - - query := `SELECT SUM(quantity) AS total_quark_value, SUM(usd_market_value) AS total_usd_value FROM ` + intentTableName + ` - WHERE phone_number = $1 AND created_at >= $2 AND intent_type = $3 AND is_deposit AND state != $4 - ` - err := db.GetContext( - ctx, - &res, - query, - phoneNumber, - since, - intent.ReceivePaymentsPrivately, - intent.StateRevoked, - ) - if err != nil { - return 0, 0, err - } - - if !res.TotalQuarkValue.Valid || !res.TotalUsdMarketValue.Valid { - return 0, 0, nil - } - return uint64(res.TotalQuarkValue.Int64), res.TotalUsdMarketValue.Float64, nil -} - -func dbGetNetBalanceFromPrePrivacy2022Intents(ctx context.Context, db *sqlx.DB, account string) (int64, error) { - var res sql.NullInt64 - - query := `SELECT - (SELECT COALESCE(SUM(quantity), 0) FROM ` + intentTableName + ` WHERE destination = $1 AND intent_type = $2 AND state IN ($3, $4, $5)) - - (SELECT COALESCE(SUM(quantity), 0) FROM ` + intentTableName + ` WHERE source = $1 AND intent_type = $2 AND state IN ($3, $4, $5));` - err := db.GetContext( - ctx, - &res, - query, - account, - intent.LegacyPayment, - intent.StatePending, - intent.StateConfirmed, - intent.StateFailed, - ) - if err != nil { - return 0, err - } - - if !res.Valid { - return 0, nil - } - return res.Int64, nil -} - -func dbGetLatestSaveRecentRootIntentForTreasury(ctx context.Context, db *sqlx.DB, treasury string) (*intentModel, error) { - res := &intentModel{} - - query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, treasury_pool, recent_root, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, is_tip, is_chat, relationship_to, tip_platform, tipped_username, chat_id, phone_number, state, created_at - FROM ` + intentTableName + ` - WHERE treasury_pool = $1 and intent_type = $2 - ORDER BY id DESC - LIMIT 1 - ` - - err := db.GetContext(ctx, res, query, treasury, intent.SaveRecentRoot) - if err != nil { - return nil, pgutil.CheckNoRows(err, intent.ErrIntentNotFound) - } - return res, nil -} - func dbGetOriginalGiftCardIssuedIntent(ctx context.Context, db *sqlx.DB, giftCardVault string) (*intentModel, error) { res := []*intentModel{} - query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, treasury_pool, recent_root, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, is_tip, is_chat, relationship_to, tip_platform, tipped_username, chat_id, phone_number, state, created_at + query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, extended_metadata, state, created_at FROM ` + intentTableName + ` WHERE destination = $1 and intent_type = $2 AND state != $3 AND is_remote_send IS TRUE LIMIT 2 @@ -609,7 +262,7 @@ func dbGetOriginalGiftCardIssuedIntent(ctx context.Context, db *sqlx.DB, giftCar func dbGetGiftCardClaimedIntent(ctx context.Context, db *sqlx.DB, giftCardVault string) (*intentModel, error) { res := []*intentModel{} - query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, treasury_pool, recent_root, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, is_tip, is_chat, relationship_to, tip_platform, tipped_username, chat_id, phone_number, state, created_at + query := `SELECT id, intent_id, intent_type, owner, source, destination_owner, destination, quantity, exchange_currency, exchange_rate, native_amount, usd_market_value, is_withdraw, is_deposit, is_remote_send, is_returned, is_issuer_voiding_gift_card, is_micro_payment, extended_metadata, state, created_at FROM ` + intentTableName + ` WHERE source = $1 and intent_type = $2 AND state != $3 AND is_remote_send IS TRUE LIMIT 2 diff --git a/pkg/code/data/intent/postgres/store.go b/pkg/code/data/intent/postgres/store.go index 6ea17af9..8c64d04f 100644 --- a/pkg/code/data/intent/postgres/store.go +++ b/pkg/code/data/intent/postgres/store.go @@ -3,9 +3,8 @@ package postgres import ( "context" "database/sql" - "time" + "errors" - "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/jmoiron/sqlx" ) @@ -62,82 +61,32 @@ func (s *store) GetLatestByInitiatorAndType(ctx context.Context, intentType inte return fromIntentModel(model), nil } -// GetAllByOwner returns all records for a given owner (as both a source and destination). -// -// Returns ErrNotFound if no records are found. -func (s *store) GetAllByOwner(ctx context.Context, owner string, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*intent.Record, error) { - models, err := dbGetAllByOwner(ctx, s.db, owner, cursor, limit, direction) - if err != nil { - return nil, err - } - - intents := make([]*intent.Record, len(models)) - for i, model := range models { - intents[i] = fromIntentModel(model) - } - - return intents, nil -} - -// CountForAntispam gets a count of intents for antispam purposes. It calculates the -// number of intents by type and state for a phone number since a timestamp. -func (s *store) CountForAntispam(ctx context.Context, intentType intent.Type, phoneNumber string, states []intent.State, since time.Time) (uint64, error) { - return dbGetCountForAntispam(ctx, s.db, intentType, phoneNumber, states, since) -} - -// CountOwnerInteractionsForAntispam gets a count of intents for antispam purposes. It -// calculates the number of times a source owner is involved in an intent with the -// destination owner since a timestamp. -func (s *store) CountOwnerInteractionsForAntispam(ctx context.Context, sourceOwner, destinationOwner string, states []intent.State, since time.Time) (uint64, error) { - return dbGetCountOwnerInteractionsForAntispam(ctx, s.db, sourceOwner, destinationOwner, states, since) -} - -// GetTransactedAmountForAntiMoneyLaundering gets the total transacted Kin in quarks and the -// corresponding USD market value for a phone number since a timestamp. -func (s *store) GetTransactedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) { - return dbGetTransactedAmountForAntiMoneyLaundering(ctx, s.db, phoneNumber, since) -} - -// GetDepositedAmountForAntiMoneyLaundering gets the total deposited Kin in quarks and the -// corresponding USD market value for a phone number since a timestamp. -func (s *store) GetDepositedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) { - return dbGetDepositedAmountForAntiMoneyLaundering(ctx, s.db, phoneNumber, since) -} - -// GetNetBalanceFromPrePrivacy2022Intents gets the net balance of Kin in quarks after appying -// pre-privacy legacy payment intents when intents detailed the entirety of the payment. -func (s *store) GetNetBalanceFromPrePrivacy2022Intents(ctx context.Context, account string) (int64, error) { - return dbGetNetBalanceFromPrePrivacy2022Intents(ctx, s.db, account) -} - -// GetLatestSaveRecentRootIntentForTreasury gets the latest SaveRecentRoot intent for a treasury -func (s *store) GetLatestSaveRecentRootIntentForTreasury(ctx context.Context, treasury string) (*intent.Record, error) { - model, err := dbGetLatestSaveRecentRootIntentForTreasury(ctx, s.db, treasury) - if err != nil { - return nil, err - } - - return fromIntentModel(model), nil -} - // GetOriginalGiftCardIssuedIntent gets the original intent where a gift card // was issued by its vault address. func (s *store) GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { - model, err := dbGetOriginalGiftCardIssuedIntent(ctx, s.db, giftCardVault) - if err != nil { - return nil, err - } + return nil, errors.New("not implemented") - return fromIntentModel(model), nil + /* + model, err := dbGetOriginalGiftCardIssuedIntent(ctx, s.db, giftCardVault) + if err != nil { + return nil, err + } + + return fromIntentModel(model), nil + */ } // GetGiftCardClaimedIntent gets the intent where a gift card was claimed by its // vault address func (s *store) GetGiftCardClaimedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { - model, err := dbGetGiftCardClaimedIntent(ctx, s.db, giftCardVault) - if err != nil { - return nil, err - } + return nil, errors.New("not implemented") - return fromIntentModel(model), nil + /* + model, err := dbGetGiftCardClaimedIntent(ctx, s.db, giftCardVault) + if err != nil { + return nil, err + } + + return fromIntentModel(model), nil + */ } diff --git a/pkg/code/data/intent/postgres/store_test.go b/pkg/code/data/intent/postgres/store_test.go index 18a7d1c9..2d96f9ac 100644 --- a/pkg/code/data/intent/postgres/store_test.go +++ b/pkg/code/data/intent/postgres/store_test.go @@ -32,9 +32,6 @@ const ( quantity bigint NULL CHECK (quantity >= 0), - treasury_pool text NULL, - recent_root text NULL, - exchange_currency varchar(3) NULL, exchange_rate numeric(18, 9) NULL, native_amount numeric(18, 9) NULL, @@ -46,16 +43,8 @@ const ( is_returned BOOL NOT NULL, is_issuer_voiding_gift_card BOOL NOT NULL, is_micro_payment BOOL NOT NULL, - is_tip BOOL NOT NULL, - is_chat BOOL NOT NULL, - - relationship_to TEXT NULL, - - tip_platform INTEGER NULL, - tipped_username TEXT NULL, - chat_id TEXT NULL, - phone_number TEXT NULL, + extended_metadata BYTEA NULL, state integer NOT NULL, diff --git a/pkg/code/data/intent/store.go b/pkg/code/data/intent/store.go index 5bc0f784..963d1037 100644 --- a/pkg/code/data/intent/store.go +++ b/pkg/code/data/intent/store.go @@ -2,9 +2,6 @@ package intent import ( "context" - "time" - - "github.com/code-payments/code-server/pkg/database/query" ) type Store interface { @@ -16,40 +13,11 @@ type Store interface { // Returns ErrNotFound if no record is found. Get(ctx context.Context, intentID string) (*Record, error) - // GetAllByOwner returns all records for a given owner (as both a source and destination). - // - // Returns ErrNotFound if no records are found. - GetAllByOwner(ctx context.Context, owner string, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) - // GetLatestByInitiatorAndType gets the latest record by initiating owner and intent type // // Returns ErrNotFound if no records are found. GetLatestByInitiatorAndType(ctx context.Context, intentType Type, owner string) (*Record, error) - // CountForAntispam gets a count of intents for antispam purposes. It calculates the - // number of intents by type and state for a phone number since a timestamp. - CountForAntispam(ctx context.Context, intentType Type, phoneNumber string, states []State, since time.Time) (uint64, error) - - // CountOwnerInteractionsForAntispam gets a count of intents for antispam purposes. It - // calculates the number of times a source owner is involved in an intent with the - // destination owner since a timestamp. - CountOwnerInteractionsForAntispam(ctx context.Context, sourceOwner, destinationOwner string, states []State, since time.Time) (uint64, error) - - // GetTransactedAmountForAntiMoneyLaundering gets the total transacted Kin in quarks and the - // corresponding USD market value for a phone number since a timestamp. - GetTransactedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) - - // GetDepositedAmountForAntiMoneyLaundering gets the total deposited Kin in quarks and the - // corresponding USD market value for a phone number since a timestamp. - GetDepositedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) - - // GetNetBalanceFromPrePrivacy2022Intents gets the net balance of Kin in quarks after appying - // pre-privacy legacy payment intents when intents detailed the entirety of the payment. - GetNetBalanceFromPrePrivacy2022Intents(ctx context.Context, account string) (int64, error) - - // GetLatestSaveRecentRootIntentForTreasury gets the latest SaveRecentRoot intent for a treasury - GetLatestSaveRecentRootIntentForTreasury(ctx context.Context, treasury string) (*Record, error) - // GetOriginalGiftCardIssuedIntent gets the original intent where a gift card // was issued by its vault address. GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*Record, error) diff --git a/pkg/code/data/intent/tests/tests.go b/pkg/code/data/intent/tests/tests.go index 2769b092..35446c9c 100644 --- a/pkg/code/data/intent/tests/tests.go +++ b/pkg/code/data/intent/tests/tests.go @@ -8,132 +8,26 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/currency" ) func RunTests(t *testing.T, s intent.Store, teardown func()) { for _, tf := range []func(t *testing.T, s intent.Store){ - testLegacyPaymentRoundTrip, - testLegacyCreateAccountRoundTrip, testOpenAccountsRoundTrip, - testSendPrivatePaymentRoundTrip, - testReceivePaymentsPrivatelyRoundTrip, - testSaveRecentRootRoundTrip, - testMigrateToPrivacy2022RoundTrip, testExternalDepositRoundTrip, testSendPublicPaymentRoundTrip, testReceivePaymentsPubliclyRoundTrip, - testEstablishRelationshipRoundTrip, - testLoginRoundTrip, testUpdate, testGetLatestByInitiatorAndType, - testGetCountForAntispam, - testGetOwnerInteractionCountForAntispam, - testGetTransactedAmountForAntiMoneyLaundering, - testGetDepositedAmountForAntiMoneyLaundering, - testGetNetBalanceFromPrePrivacyIntents, - testGetLatestSaveRecentRootIntentForTreasury, testGetOriginalGiftCardIssuedIntent, testGetGiftCardClaimedIntent, - testChatPayment, } { tf(t, s) teardown() } } -func testLegacyPaymentRoundTrip(t *testing.T, s intent.Store) { - t.Run("testLegacyPaymentRoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.Get(ctx, "test_intent_id") - require.Error(t, err) - assert.Equal(t, intent.ErrIntentNotFound, err) - assert.Nil(t, actual) - - phoneNumberValue := "+12223334444" - expected := intent.Record{ - IntentId: "test_intent_id", - IntentType: intent.LegacyPayment, - InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, - MoneyTransferMetadata: &intent.MoneyTransferMetadata{ - Source: "test_source", - Destination: "test_destination", - Quantity: 42, - ExchangeCurrency: currency.CAD, - ExchangeRate: 0.00073, - UsdMarketValue: 0.00042, - IsWithdrawal: true, - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Save(ctx, &expected) - require.NoError(t, err) - - actual, err = s.Get(ctx, "test_intent_id") - require.NoError(t, err) - assert.Equal(t, cloned.IntentId, actual.IntentId) - assert.Equal(t, cloned.IntentType, actual.IntentType) - assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) - require.NotNil(t, actual.MoneyTransferMetadata) - assert.Equal(t, cloned.MoneyTransferMetadata.Source, actual.MoneyTransferMetadata.Source) - assert.Equal(t, cloned.MoneyTransferMetadata.Destination, actual.MoneyTransferMetadata.Destination) - assert.Equal(t, cloned.MoneyTransferMetadata.Quantity, actual.MoneyTransferMetadata.Quantity) - assert.Equal(t, cloned.MoneyTransferMetadata.ExchangeCurrency, actual.MoneyTransferMetadata.ExchangeCurrency) - assert.Equal(t, cloned.MoneyTransferMetadata.ExchangeRate, actual.MoneyTransferMetadata.ExchangeRate) - assert.Equal(t, cloned.MoneyTransferMetadata.UsdMarketValue, actual.MoneyTransferMetadata.UsdMarketValue) - assert.Equal(t, cloned.MoneyTransferMetadata.IsWithdrawal, actual.MoneyTransferMetadata.IsWithdrawal) - assert.Equal(t, cloned.State, actual.State) - assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assert.EqualValues(t, 1, actual.Id) - }) -} - -func testLegacyCreateAccountRoundTrip(t *testing.T, s intent.Store) { - t.Run("testLegacyCreateAccountRoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.Get(ctx, "test_intent_id") - require.Error(t, err) - assert.Equal(t, intent.ErrIntentNotFound, err) - assert.Nil(t, actual) - - phoneNumberValue := "+12223334444" - expected := intent.Record{ - IntentId: "test_intent_id", - IntentType: intent.LegacyCreateAccount, - InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, - AccountManagementMetadata: &intent.AccountManagementMetadata{ - TokenAccount: "test_account", - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Save(ctx, &expected) - require.NoError(t, err) - - actual, err = s.Get(ctx, "test_intent_id") - require.NoError(t, err) - assert.Equal(t, cloned.IntentId, actual.IntentId) - assert.Equal(t, cloned.IntentType, actual.IntentType) - assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) - require.NotNil(t, actual.AccountManagementMetadata) - assert.Equal(t, cloned.AccountManagementMetadata.TokenAccount, actual.AccountManagementMetadata.TokenAccount) - assert.Equal(t, cloned.State, actual.State) - assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assert.EqualValues(t, 1, actual.Id) - }) -} - func testOpenAccountsRoundTrip(t *testing.T, s intent.Store) { t.Run("testOpenAccountsRoundTrip", func(t *testing.T) { ctx := context.Background() @@ -143,13 +37,12 @@ func testOpenAccountsRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, intent.ErrIntentNotFound, err) assert.Nil(t, actual) - phoneNumberValue := "+12223334444" expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, + ExtendedMetadata: []byte("extended_metadata"), State: intent.StateUnknown, CreatedAt: time.Now(), } @@ -162,200 +55,8 @@ func testOpenAccountsRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) require.NotNil(t, actual.OpenAccountsMetadata) - assert.Equal(t, cloned.State, actual.State) - assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assert.EqualValues(t, 1, actual.Id) - }) -} - -func testSendPrivatePaymentRoundTrip(t *testing.T, s intent.Store) { - t.Run("testSendPrivatePaymentRoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.Get(ctx, "test_intent_id") - require.Error(t, err) - assert.Equal(t, intent.ErrIntentNotFound, err) - assert.Nil(t, actual) - - phoneNumberValue := "+12223334444" - expected := intent.Record{ - IntentId: "test_intent_id", - IntentType: intent.SendPrivatePayment, - InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - DestinationOwnerAccount: "test_destination_owner", - DestinationTokenAccount: "test_destination_token", - Quantity: 12345, - - ExchangeCurrency: currency.CAD, - ExchangeRate: 0.00073, - NativeAmount: 0.00073 * 12345, - UsdMarketValue: 0.00042, - - IsWithdrawal: true, - IsRemoteSend: true, - IsMicroPayment: true, - IsTip: true, - - TipMetadata: &intent.TipMetadata{ - Platform: transaction.TippedUser_TWITTER, - Username: "tipme", - }, - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Save(ctx, &expected) - require.NoError(t, err) - - actual, err = s.Get(ctx, "test_intent_id") - require.NoError(t, err) - assert.Equal(t, cloned.IntentId, actual.IntentId) - assert.Equal(t, cloned.IntentType, actual.IntentType) - assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) - require.NotNil(t, actual.SendPrivatePaymentMetadata) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.DestinationOwnerAccount, actual.SendPrivatePaymentMetadata.DestinationOwnerAccount) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.DestinationTokenAccount, actual.SendPrivatePaymentMetadata.DestinationTokenAccount) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.Quantity, actual.SendPrivatePaymentMetadata.Quantity) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.ExchangeCurrency, actual.SendPrivatePaymentMetadata.ExchangeCurrency) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.ExchangeRate, actual.SendPrivatePaymentMetadata.ExchangeRate) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.NativeAmount, actual.SendPrivatePaymentMetadata.NativeAmount) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.UsdMarketValue, actual.SendPrivatePaymentMetadata.UsdMarketValue) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.IsWithdrawal, actual.SendPrivatePaymentMetadata.IsWithdrawal) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.IsRemoteSend, actual.SendPrivatePaymentMetadata.IsRemoteSend) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.IsMicroPayment, actual.SendPrivatePaymentMetadata.IsMicroPayment) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.IsTip, actual.SendPrivatePaymentMetadata.IsTip) - require.NotNil(t, actual.SendPrivatePaymentMetadata.TipMetadata) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.TipMetadata.Platform, actual.SendPrivatePaymentMetadata.TipMetadata.Platform) - assert.Equal(t, cloned.SendPrivatePaymentMetadata.TipMetadata.Username, actual.SendPrivatePaymentMetadata.TipMetadata.Username) - assert.Equal(t, cloned.State, actual.State) - assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assert.EqualValues(t, 1, actual.Id) - }) -} - -func testReceivePaymentsPrivatelyRoundTrip(t *testing.T, s intent.Store) { - t.Run("testReceivePaymentsPrivatelyRoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.Get(ctx, "test_intent_id") - require.Error(t, err) - assert.Equal(t, intent.ErrIntentNotFound, err) - assert.Nil(t, actual) - - phoneNumberValue := "+12223334444" - expected := intent.Record{ - IntentId: "test_intent_id", - IntentType: intent.ReceivePaymentsPrivately, - InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, - ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{ - Source: "test_source", - Quantity: 12345, - IsDeposit: true, - UsdMarketValue: 777, - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Save(ctx, &expected) - require.NoError(t, err) - - actual, err = s.Get(ctx, "test_intent_id") - require.NoError(t, err) - assert.Equal(t, cloned.IntentId, actual.IntentId) - assert.Equal(t, cloned.IntentType, actual.IntentType) - assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) - require.NotNil(t, actual.ReceivePaymentsPrivatelyMetadata) - assert.Equal(t, cloned.ReceivePaymentsPrivatelyMetadata.Source, actual.ReceivePaymentsPrivatelyMetadata.Source) - assert.Equal(t, cloned.ReceivePaymentsPrivatelyMetadata.Quantity, actual.ReceivePaymentsPrivatelyMetadata.Quantity) - assert.Equal(t, cloned.ReceivePaymentsPrivatelyMetadata.IsDeposit, actual.ReceivePaymentsPrivatelyMetadata.IsDeposit) - assert.Equal(t, cloned.ReceivePaymentsPrivatelyMetadata.UsdMarketValue, actual.ReceivePaymentsPrivatelyMetadata.UsdMarketValue) - assert.Equal(t, cloned.State, actual.State) - assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assert.EqualValues(t, 1, actual.Id) - }) -} - -func testSaveRecentRootRoundTrip(t *testing.T, s intent.Store) { - t.Run("testSaveRecentRootRoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.Get(ctx, "test_intent_id") - require.Error(t, err) - assert.Equal(t, intent.ErrIntentNotFound, err) - assert.Nil(t, actual) - - expected := intent.Record{ - IntentId: "test_intent_id", - IntentType: intent.SaveRecentRoot, - InitiatorOwnerAccount: "test_owner", - SaveRecentRootMetadata: &intent.SaveRecentRootMetadata{ - TreasuryPool: "test_treasury_pool", - PreviousMostRecentRoot: "test_recent_root", - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Save(ctx, &expected) - require.NoError(t, err) - - actual, err = s.Get(ctx, "test_intent_id") - require.NoError(t, err) - assert.Equal(t, cloned.IntentId, actual.IntentId) - assert.Equal(t, cloned.IntentType, actual.IntentType) - assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Nil(t, actual.InitiatorPhoneNumber) - require.NotNil(t, actual.SaveRecentRootMetadata) - assert.Equal(t, cloned.SaveRecentRootMetadata.TreasuryPool, actual.SaveRecentRootMetadata.TreasuryPool) - assert.Equal(t, cloned.SaveRecentRootMetadata.PreviousMostRecentRoot, actual.SaveRecentRootMetadata.PreviousMostRecentRoot) - assert.Equal(t, cloned.State, actual.State) - assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assert.EqualValues(t, 1, actual.Id) - }) -} - -func testMigrateToPrivacy2022RoundTrip(t *testing.T, s intent.Store) { - t.Run("testMigrateToPrivacy2022RoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.Get(ctx, "test_intent_id") - require.Error(t, err) - assert.Equal(t, intent.ErrIntentNotFound, err) - assert.Nil(t, actual) - - phoneNumberValue := "+12223334444" - expected := intent.Record{ - IntentId: "test_intent_id", - IntentType: intent.MigrateToPrivacy2022, - InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, - MigrateToPrivacy2022Metadata: &intent.MigrateToPrivacy2022Metadata{ - Quantity: 123, - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Save(ctx, &expected) - require.NoError(t, err) - - actual, err = s.Get(ctx, "test_intent_id") - require.NoError(t, err) - assert.Equal(t, cloned.IntentId, actual.IntentId) - assert.Equal(t, cloned.IntentType, actual.IntentType) - assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) - require.NotNil(t, actual.MigrateToPrivacy2022Metadata) - assert.Equal(t, cloned.MigrateToPrivacy2022Metadata.Quantity, actual.MigrateToPrivacy2022Metadata.Quantity) + assert.Equal(t, cloned.ExtendedMetadata, actual.ExtendedMetadata) assert.Equal(t, cloned.State, actual.State) assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) assert.EqualValues(t, 1, actual.Id) @@ -381,8 +82,9 @@ func testExternalDepositRoundTrip(t *testing.T, s intent.Store) { Quantity: 12345, UsdMarketValue: 1.2345, }, - State: intent.StateUnknown, - CreatedAt: time.Now(), + ExtendedMetadata: []byte("extended_metadata"), + State: intent.StateUnknown, + CreatedAt: time.Now(), } cloned := expected.Clone() err = s.Save(ctx, &expected) @@ -393,12 +95,12 @@ func testExternalDepositRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Nil(t, actual.InitiatorPhoneNumber) require.NotNil(t, actual.ExternalDepositMetadata) assert.Equal(t, cloned.ExternalDepositMetadata.DestinationOwnerAccount, actual.ExternalDepositMetadata.DestinationOwnerAccount) assert.Equal(t, cloned.ExternalDepositMetadata.DestinationTokenAccount, actual.ExternalDepositMetadata.DestinationTokenAccount) assert.Equal(t, cloned.ExternalDepositMetadata.Quantity, actual.ExternalDepositMetadata.Quantity) assert.Equal(t, cloned.ExternalDepositMetadata.UsdMarketValue, actual.ExternalDepositMetadata.UsdMarketValue) + assert.Equal(t, cloned.ExtendedMetadata, actual.ExtendedMetadata) assert.Equal(t, cloned.State, actual.State) assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) assert.EqualValues(t, 1, actual.Id) @@ -414,12 +116,10 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, intent.ErrIntentNotFound, err) assert.Nil(t, actual) - phoneNumberValue := "+12223334444" expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ DestinationOwnerAccount: "test_destination_owner", DestinationTokenAccount: "test_destination_token", @@ -431,8 +131,9 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { UsdMarketValue: 0.00042, IsWithdrawal: true, }, - State: intent.StateUnknown, - CreatedAt: time.Now(), + ExtendedMetadata: []byte("extended_metadata"), + State: intent.StateUnknown, + CreatedAt: time.Now(), } cloned := expected.Clone() err = s.Save(ctx, &expected) @@ -443,7 +144,6 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) require.NotNil(t, actual.SendPublicPaymentMetadata) assert.Equal(t, cloned.SendPublicPaymentMetadata.DestinationOwnerAccount, actual.SendPublicPaymentMetadata.DestinationOwnerAccount) assert.Equal(t, cloned.SendPublicPaymentMetadata.DestinationTokenAccount, actual.SendPublicPaymentMetadata.DestinationTokenAccount) @@ -453,6 +153,7 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, cloned.SendPublicPaymentMetadata.NativeAmount, actual.SendPublicPaymentMetadata.NativeAmount) assert.Equal(t, cloned.SendPublicPaymentMetadata.UsdMarketValue, actual.SendPublicPaymentMetadata.UsdMarketValue) assert.Equal(t, cloned.SendPublicPaymentMetadata.IsWithdrawal, actual.SendPublicPaymentMetadata.IsWithdrawal) + assert.Equal(t, cloned.ExtendedMetadata, actual.ExtendedMetadata) assert.Equal(t, cloned.State, actual.State) assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) assert.EqualValues(t, 1, actual.Id) @@ -468,12 +169,10 @@ func testReceivePaymentsPubliclyRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, intent.ErrIntentNotFound, err) assert.Nil(t, actual) - phoneNumberValue := "+12223334444" expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.ReceivePaymentsPublicly, InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{ Source: "test_source", Quantity: 12345, @@ -485,8 +184,9 @@ func testReceivePaymentsPubliclyRoundTrip(t *testing.T, s intent.Store) { OriginalNativeAmount: 1234.5, UsdMarketValue: 999.99, }, - State: intent.StateUnknown, - CreatedAt: time.Now(), + ExtendedMetadata: []byte("extended_metadata"), + State: intent.StateUnknown, + CreatedAt: time.Now(), } cloned := expected.Clone() err = s.Save(ctx, &expected) @@ -497,7 +197,6 @@ func testReceivePaymentsPubliclyRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) require.NotNil(t, actual.ReceivePaymentsPubliclyMetadata) assert.Equal(t, cloned.ReceivePaymentsPubliclyMetadata.Source, actual.ReceivePaymentsPubliclyMetadata.Source) assert.Equal(t, cloned.ReceivePaymentsPubliclyMetadata.Quantity, actual.ReceivePaymentsPubliclyMetadata.Quantity) @@ -508,86 +207,7 @@ func testReceivePaymentsPubliclyRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, cloned.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate, actual.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate) assert.Equal(t, cloned.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount, actual.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount) assert.Equal(t, cloned.ReceivePaymentsPubliclyMetadata.UsdMarketValue, actual.ReceivePaymentsPubliclyMetadata.UsdMarketValue) - assert.Equal(t, cloned.State, actual.State) - assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assert.EqualValues(t, 1, actual.Id) - }) -} - -func testEstablishRelationshipRoundTrip(t *testing.T, s intent.Store) { - t.Run("testEstablishRelationshipRoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.Get(ctx, "test_intent_id") - require.Error(t, err) - assert.Equal(t, intent.ErrIntentNotFound, err) - assert.Nil(t, actual) - - phoneNumberValue := "+12223334444" - expected := intent.Record{ - IntentId: "test_intent_id", - IntentType: intent.EstablishRelationship, - InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, - EstablishRelationshipMetadata: &intent.EstablishRelationshipMetadata{ - RelationshipTo: "relationship_to", - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Save(ctx, &expected) - require.NoError(t, err) - - actual, err = s.Get(ctx, "test_intent_id") - require.NoError(t, err) - assert.Equal(t, cloned.IntentId, actual.IntentId) - assert.Equal(t, cloned.IntentType, actual.IntentType) - assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) - require.NotNil(t, actual.EstablishRelationshipMetadata) - assert.Equal(t, cloned.EstablishRelationshipMetadata.RelationshipTo, actual.EstablishRelationshipMetadata.RelationshipTo) - assert.Equal(t, cloned.State, actual.State) - assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) - assert.EqualValues(t, 1, actual.Id) - }) -} - -func testLoginRoundTrip(t *testing.T, s intent.Store) { - t.Run("testLoginRoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.Get(ctx, "test_intent_id") - require.Error(t, err) - assert.Equal(t, intent.ErrIntentNotFound, err) - assert.Nil(t, actual) - - phoneNumberValue := "+12223334444" - expected := intent.Record{ - IntentId: "test_intent_id", - IntentType: intent.Login, - InitiatorOwnerAccount: "test_owner", - InitiatorPhoneNumber: &phoneNumberValue, - LoginMetadata: &intent.LoginMetadata{ - App: "app", - UserId: "test_user", - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Save(ctx, &expected) - require.NoError(t, err) - - actual, err = s.Get(ctx, "test_intent_id") - require.NoError(t, err) - assert.Equal(t, cloned.IntentId, actual.IntentId) - assert.Equal(t, cloned.IntentType, actual.IntentType) - assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) - assert.Equal(t, *cloned.InitiatorPhoneNumber, *actual.InitiatorPhoneNumber) - require.NotNil(t, actual.LoginMetadata) - assert.Equal(t, cloned.LoginMetadata.App, actual.LoginMetadata.App) - assert.Equal(t, cloned.LoginMetadata.UserId, actual.LoginMetadata.UserId) + assert.Equal(t, cloned.ExtendedMetadata, actual.ExtendedMetadata) assert.Equal(t, cloned.State, actual.State) assert.Equal(t, cloned.CreatedAt.Unix(), actual.CreatedAt.Unix()) assert.EqualValues(t, 1, actual.Id) @@ -600,18 +220,11 @@ func testUpdate(t *testing.T, s intent.Store) { expected := intent.Record{ IntentId: "test_intent_id", - IntentType: intent.LegacyPayment, + IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "test_owner", - MoneyTransferMetadata: &intent.MoneyTransferMetadata{ - Source: "test_source", - Destination: "test_destination", - Quantity: 42, - ExchangeCurrency: currency.CAD, - ExchangeRate: 0.00073, - UsdMarketValue: 0.00042, - }, - State: intent.StateUnknown, - CreatedAt: time.Now(), + OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, + State: intent.StateUnknown, + CreatedAt: time.Now(), } err := s.Save(ctx, &expected) require.NoError(t, err) @@ -634,432 +247,113 @@ func testGetLatestByInitiatorAndType(t *testing.T, s intent.Store) { t.Run("testGetLatestByInitiatorAndType", func(t *testing.T) { records := []intent.Record{ - {IntentId: "t1", IntentType: intent.LegacyPayment, InitiatorOwnerAccount: "o1", MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a3", Quantity: 42, ExchangeCurrency: currency.CAD, ExchangeRate: 0.0007, UsdMarketValue: 0.01}, State: intent.StatePending}, - {IntentId: "t2", IntentType: intent.LegacyPayment, InitiatorOwnerAccount: "o1", MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a4", Quantity: 42, ExchangeCurrency: currency.CAD, ExchangeRate: 0.0007, UsdMarketValue: 0.01}, State: intent.StateFailed}, - {IntentId: "t3", IntentType: intent.LegacyPayment, InitiatorOwnerAccount: "o1", MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a5", Quantity: 42, ExchangeCurrency: currency.CAD, ExchangeRate: 0.0007, UsdMarketValue: 0.01}, State: intent.StateUnknown}, - {IntentId: "t4", IntentType: intent.LegacyPayment, InitiatorOwnerAccount: "o1", MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a6", Quantity: 42, ExchangeCurrency: currency.CAD, ExchangeRate: 0.0007, UsdMarketValue: 0.01}, State: intent.StateUnknown}, - {IntentId: "t5", IntentType: intent.LegacyPayment, InitiatorOwnerAccount: "o2", MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a2", Destination: "a7", Quantity: 42, ExchangeCurrency: currency.CAD, ExchangeRate: 0.0007, UsdMarketValue: 0.01}, State: intent.StateUnknown}, - {IntentId: "t6", IntentType: intent.LegacyPayment, InitiatorOwnerAccount: "o2", MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a2", Destination: "a8", Quantity: 42, ExchangeCurrency: currency.CAD, ExchangeRate: 0.0007, UsdMarketValue: 0.01}, State: intent.StateFailed}, + {IntentId: "t1", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "o1", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, State: intent.StatePending}, + {IntentId: "t2", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "o1", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, State: intent.StateFailed}, + {IntentId: "t3", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "o1", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, State: intent.StateUnknown}, + {IntentId: "t4", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "o1", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, State: intent.StateUnknown}, + {IntentId: "t5", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "o2", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, State: intent.StateUnknown}, + {IntentId: "t6", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "o2", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, State: intent.StateFailed}, } for i, record := range records { record.CreatedAt = time.Now().Add(time.Duration(i) * time.Second) require.NoError(t, s.Save(ctx, &record)) } - _, err := s.GetLatestByInitiatorAndType(ctx, intent.LegacyCreateAccount, "o1") + _, err := s.GetLatestByInitiatorAndType(ctx, intent.SendPublicPayment, "o1") assert.Equal(t, intent.ErrIntentNotFound, err) - actual, err := s.GetLatestByInitiatorAndType(ctx, intent.LegacyPayment, "o1") + actual, err := s.GetLatestByInitiatorAndType(ctx, intent.OpenAccounts, "o1") require.NoError(t, err) assert.Equal(t, "t4", actual.IntentId) }) } -func testGetCountForAntispam(t *testing.T, s intent.Store) { - t.Run("testGetCountForAntispam", func(t *testing.T) { - ctx := context.Background() - - phoneNumber := "+12223334444" - otherPhoneNumber := "+18005550000" - - records := []intent.Record{ - {IntentId: "i1", IntentType: intent.LegacyCreateAccount, InitiatorOwnerAccount: "o1", InitiatorPhoneNumber: &phoneNumber, AccountManagementMetadata: &intent.AccountManagementMetadata{TokenAccount: "t1"}, State: intent.StateUnknown, CreatedAt: time.Now().Add(-1 * time.Minute)}, - {IntentId: "i2", IntentType: intent.LegacyCreateAccount, InitiatorOwnerAccount: "o2", InitiatorPhoneNumber: &phoneNumber, AccountManagementMetadata: &intent.AccountManagementMetadata{TokenAccount: "t2"}, State: intent.StatePending, CreatedAt: time.Now().Add(-2 * time.Minute)}, - {IntentId: "i3", IntentType: intent.LegacyCreateAccount, InitiatorOwnerAccount: "o3", InitiatorPhoneNumber: &phoneNumber, AccountManagementMetadata: &intent.AccountManagementMetadata{TokenAccount: "t3"}, State: intent.StateConfirmed, CreatedAt: time.Now().Add(-3 * time.Minute)}, - {IntentId: "i4", IntentType: intent.LegacyCreateAccount, InitiatorOwnerAccount: "o4", InitiatorPhoneNumber: &phoneNumber, AccountManagementMetadata: &intent.AccountManagementMetadata{TokenAccount: "t4"}, State: intent.StateFailed, CreatedAt: time.Now().Add(-4 * time.Minute)}, - {IntentId: "i5", IntentType: intent.LegacyCreateAccount, InitiatorOwnerAccount: "o5", InitiatorPhoneNumber: &phoneNumber, AccountManagementMetadata: &intent.AccountManagementMetadata{TokenAccount: "t5"}, State: intent.StateRevoked, CreatedAt: time.Now().Add(-5 * time.Minute)}, - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } - - allStates := []intent.State{ - intent.StateUnknown, - intent.StatePending, - intent.StateConfirmed, - intent.StateFailed, - intent.StateRevoked, - } - - // Capture all intents for the phone number - count, err := s.CountForAntispam(ctx, intent.LegacyCreateAccount, phoneNumber, allStates, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 5, count) - - // Capture a subset of intents based on time - count, err = s.CountForAntispam(ctx, intent.LegacyCreateAccount, phoneNumber, allStates, time.Now().Add(-90*time.Second)) - require.NoError(t, err) - assert.EqualValues(t, 1, count) - - // Capture a subset of intents based on state - count, err = s.CountForAntispam(ctx, intent.LegacyCreateAccount, phoneNumber, []intent.State{intent.StateUnknown, intent.StatePending}, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 2, count) - - // Capture no intents because the type mismatches - count, err = s.CountForAntispam(ctx, intent.LegacyPayment, phoneNumber, allStates, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - // Capture no intents because the phone number mismatches - count, err = s.CountForAntispam(ctx, intent.LegacyCreateAccount, otherPhoneNumber, allStates, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - }) -} - -func testGetOwnerInteractionCountForAntispam(t *testing.T, s intent.Store) { - t.Run("testGetOwnerInteractionCountForAntispam", func(t *testing.T) { - ctx := context.Background() - - phoneNumber := "+12223334444" - records := []intent.Record{ - {IntentId: "i1", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o1", InitiatorPhoneNumber: &phoneNumber, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "t2", Quantity: 1, ExchangeCurrency: currency.KIN, ExchangeRate: 1.0, NativeAmount: 1.0, UsdMarketValue: 1.0}, State: intent.StateUnknown, CreatedAt: time.Now().Add(-1 * time.Minute)}, - {IntentId: "i2", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o1", InitiatorPhoneNumber: &phoneNumber, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "t2", Quantity: 1, ExchangeCurrency: currency.KIN, ExchangeRate: 1.0, NativeAmount: 1.0, UsdMarketValue: 1.0}, State: intent.StatePending, CreatedAt: time.Now().Add(-2 * time.Minute)}, - {IntentId: "i3", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o1", InitiatorPhoneNumber: &phoneNumber, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "t2", Quantity: 1, ExchangeCurrency: currency.KIN, ExchangeRate: 1.0, NativeAmount: 1.0, UsdMarketValue: 1.0}, State: intent.StateConfirmed, CreatedAt: time.Now().Add(-3 * time.Minute)}, - {IntentId: "i4", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", InitiatorPhoneNumber: &phoneNumber, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "t2", Quantity: 1, ExchangeCurrency: currency.KIN, ExchangeRate: 1.0, NativeAmount: 1.0, UsdMarketValue: 1.0}, State: intent.StateFailed, CreatedAt: time.Now().Add(-4 * time.Minute)}, - {IntentId: "i5", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", InitiatorPhoneNumber: &phoneNumber, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "t2", Quantity: 1, ExchangeCurrency: currency.KIN, ExchangeRate: 1.0, NativeAmount: 1.0, UsdMarketValue: 1.0}, State: intent.StateRevoked, CreatedAt: time.Now().Add(-5 * time.Minute)}, - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } - - allStates := []intent.State{ - intent.StateUnknown, - intent.StatePending, - intent.StateConfirmed, - intent.StateFailed, - intent.StateRevoked, - } - - // Capture all intents for the owner pair - count, err := s.CountOwnerInteractionsForAntispam(ctx, "o1", "o2", allStates, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 5, count) - - // Capture no intents for incorrect owner pairs - - count, err = s.CountOwnerInteractionsForAntispam(ctx, "o2", "o1", allStates, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - count, err = s.CountOwnerInteractionsForAntispam(ctx, "o1", "o1", allStates, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - count, err = s.CountOwnerInteractionsForAntispam(ctx, "o2", "o2", allStates, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - // Capture a subset of intents for the owner pair based on time - count, err = s.CountOwnerInteractionsForAntispam(ctx, "o1", "o2", allStates, time.Now().Add(-90*time.Second)) - require.NoError(t, err) - assert.EqualValues(t, 1, count) - - // Capture a subset of intents for the owner pair based on state - count, err = s.CountOwnerInteractionsForAntispam(ctx, "o1", "o2", []intent.State{intent.StateUnknown, intent.StatePending}, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 2, count) - }) -} - -func testGetTransactedAmountForAntiMoneyLaundering(t *testing.T, s intent.Store) { - t.Run("testGetTransactedAmountForAntiMoneyLaundering", func(t *testing.T) { - ctx := context.Background() - - phoneNumber := "+12223334444" - - // No intents results in zero transacted values - quarks, usdMarketValue, err := s.GetTransactedAmountForAntiMoneyLaundering(ctx, phoneNumber, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, quarks) - assert.EqualValues(t, 0, usdMarketValue) - - records := []intent.Record{ - {IntentId: "t1", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o1", SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o1", DestinationTokenAccount: "a1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2, UsdMarketValue: 2}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateUnknown, CreatedAt: time.Now().Add(-1 * time.Minute)}, - {IntentId: "t2", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o2", SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 10, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 20, UsdMarketValue: 20}, InitiatorPhoneNumber: &phoneNumber, State: intent.StatePending, CreatedAt: time.Now().Add(-2 * time.Minute)}, - {IntentId: "t3", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o3", SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o3", DestinationTokenAccount: "a3", Quantity: 100, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 200, UsdMarketValue: 200}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateConfirmed, CreatedAt: time.Now().Add(-3 * time.Minute)}, - {IntentId: "t4", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o4", SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o4", DestinationTokenAccount: "a4", Quantity: 1000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2000, UsdMarketValue: 2000}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateFailed, CreatedAt: time.Now().Add(-4 * time.Minute)}, - {IntentId: "t5", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o5", SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o5", DestinationTokenAccount: "a5", Quantity: 10000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 20000, UsdMarketValue: 20000}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateRevoked, CreatedAt: time.Now().Add(-5 * time.Minute)}, - {IntentId: "t6", IntentType: intent.ReceivePaymentsPrivately, InitiatorOwnerAccount: "o6", ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{Source: "a6", Quantity: 100000, UsdMarketValue: 200000}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateConfirmed, CreatedAt: time.Now()}, - {IntentId: "t7", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o7", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o7", DestinationTokenAccount: "a7", Quantity: 1000000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2000000, UsdMarketValue: 20000}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateConfirmed, CreatedAt: time.Now()}, - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } - - // Capture all intents for the phone number - quarks, usdMarketValue, err = s.GetTransactedAmountForAntiMoneyLaundering(ctx, phoneNumber, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 1111, quarks) - assert.EqualValues(t, 2222, usdMarketValue) - - // Capture a subset of intents based on time - quarks, usdMarketValue, err = s.GetTransactedAmountForAntiMoneyLaundering(ctx, phoneNumber, time.Now().Add(-150*time.Second)) - require.NoError(t, err) - assert.EqualValues(t, 11, quarks) - assert.EqualValues(t, 22, usdMarketValue) - - // Capture no intents because the phone number mismatches - quarks, usdMarketValue, err = s.GetTransactedAmountForAntiMoneyLaundering(ctx, "+18005550000", time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, quarks) - assert.EqualValues(t, 0, usdMarketValue) - }) -} - -func testGetDepositedAmountForAntiMoneyLaundering(t *testing.T, s intent.Store) { - t.Run("testGetDepositedAmountForAntiMoneyLaundering", func(t *testing.T) { - ctx := context.Background() - - phoneNumber := "+12223334444" - - // No intents results in zero transacted values - quarks, usdMarketValue, err := s.GetTransactedAmountForAntiMoneyLaundering(ctx, phoneNumber, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, quarks) - assert.EqualValues(t, 0, usdMarketValue) - - records := []intent.Record{ - {IntentId: "t1", IntentType: intent.ReceivePaymentsPrivately, InitiatorOwnerAccount: "o1", ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{IsDeposit: true, Source: "a1", Quantity: 1, UsdMarketValue: 2}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateUnknown, CreatedAt: time.Now().Add(-1 * time.Minute)}, - {IntentId: "t2", IntentType: intent.ReceivePaymentsPrivately, InitiatorOwnerAccount: "o2", ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{IsDeposit: true, Source: "a2", Quantity: 10, UsdMarketValue: 20}, InitiatorPhoneNumber: &phoneNumber, State: intent.StatePending, CreatedAt: time.Now().Add(-2 * time.Minute)}, - {IntentId: "t3", IntentType: intent.ReceivePaymentsPrivately, InitiatorOwnerAccount: "o3", ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{IsDeposit: true, Source: "a3", Quantity: 100, UsdMarketValue: 200}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateConfirmed, CreatedAt: time.Now().Add(-3 * time.Minute)}, - {IntentId: "t4", IntentType: intent.ReceivePaymentsPrivately, InitiatorOwnerAccount: "o4", ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{IsDeposit: true, Source: "a4", Quantity: 1000, UsdMarketValue: 2000}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateFailed, CreatedAt: time.Now().Add(-4 * time.Minute)}, - {IntentId: "t5", IntentType: intent.ReceivePaymentsPrivately, InitiatorOwnerAccount: "o5", ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{IsDeposit: true, Source: "a5", Quantity: 10000, UsdMarketValue: 20000}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateRevoked, CreatedAt: time.Now().Add(-5 * time.Minute)}, - {IntentId: "t6", IntentType: intent.ReceivePaymentsPrivately, InitiatorOwnerAccount: "o6", ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{IsDeposit: false, Source: "a6", Quantity: 100000, UsdMarketValue: 200000}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateRevoked, CreatedAt: time.Now().Add(-5 * time.Minute)}, - {IntentId: "t7", IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: "o7", SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationOwnerAccount: "o7", DestinationTokenAccount: "a7", Quantity: 1000000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2000000, UsdMarketValue: 2000000}, InitiatorPhoneNumber: &phoneNumber, State: intent.StateRevoked, CreatedAt: time.Now().Add(-5 * time.Minute)}, - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } - - // Capture all intents for the phone number - quarks, usdMarketValue, err = s.GetDepositedAmountForAntiMoneyLaundering(ctx, phoneNumber, time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 1111, quarks) - assert.EqualValues(t, 2222, usdMarketValue) - - // Capture a subset of intents based on time - quarks, usdMarketValue, err = s.GetDepositedAmountForAntiMoneyLaundering(ctx, phoneNumber, time.Now().Add(-150*time.Second)) - require.NoError(t, err) - assert.EqualValues(t, 11, quarks) - assert.EqualValues(t, 22, usdMarketValue) - - // Capture no intents because the phone number mismatches - quarks, usdMarketValue, err = s.GetDepositedAmountForAntiMoneyLaundering(ctx, "+18005550000", time.Now().Add(-24*time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, quarks) - assert.EqualValues(t, 0, usdMarketValue) - }) -} - -func testGetNetBalanceFromPrePrivacyIntents(t *testing.T, s intent.Store) { - t.Run("testGetNetBalanceFromPrePrivacyIntents", func(t *testing.T) { - ctx := context.Background() - - records := []intent.Record{ - {IntentId: "i1", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "o1", State: intent.StateUnknown}, - {IntentId: "i2", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a2", Quantity: 10, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 10}, InitiatorOwnerAccount: "o1", State: intent.StatePending}, - {IntentId: "i3", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a2", Quantity: 100, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 100}, InitiatorOwnerAccount: "o1", State: intent.StateConfirmed}, - {IntentId: "i4", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a2", Quantity: 1000, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 1000}, InitiatorOwnerAccount: "o1", State: intent.StateFailed}, - {IntentId: "i5", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a1", Destination: "a2", Quantity: 10000, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 10000}, InitiatorOwnerAccount: "o1", State: intent.StateRevoked}, - {IntentId: "i6", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 100000, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 100000, UsdMarketValue: 100000}, InitiatorOwnerAccount: "o1", State: intent.StateConfirmed}, - {IntentId: "i7", IntentType: intent.ReceivePaymentsPrivately, ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{Source: "a2", Quantity: 100000, UsdMarketValue: 100000}, InitiatorOwnerAccount: "o1", State: intent.StateConfirmed}, - {IntentId: "i8", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a3", Destination: "a3", Quantity: 100000, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 100000}, InitiatorOwnerAccount: "o3", State: intent.StateConfirmed}, - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } - - netBalance, err := s.GetNetBalanceFromPrePrivacy2022Intents(ctx, "a1") - require.NoError(t, err) - assert.EqualValues(t, -1110, netBalance) - - netBalance, err = s.GetNetBalanceFromPrePrivacy2022Intents(ctx, "a2") - require.NoError(t, err) - assert.EqualValues(t, 1110, netBalance) - - netBalance, err = s.GetNetBalanceFromPrePrivacy2022Intents(ctx, "a3") - require.NoError(t, err) - assert.EqualValues(t, 0, netBalance) - - netBalance, err = s.GetNetBalanceFromPrePrivacy2022Intents(ctx, "a4") - require.NoError(t, err) - assert.EqualValues(t, 0, netBalance) - }) -} - -func testGetLatestSaveRecentRootIntentForTreasury(t *testing.T, s intent.Store) { - t.Run("testGetLatestSaveRecentRootIntentForTreasury", func(t *testing.T) { - ctx := context.Background() - - records := []intent.Record{ - {IntentId: "i1", IntentType: intent.SaveRecentRoot, SaveRecentRootMetadata: &intent.SaveRecentRootMetadata{TreasuryPool: "t1", PreviousMostRecentRoot: "rr1"}, InitiatorOwnerAccount: "code", State: intent.StateConfirmed}, - {IntentId: "i2", IntentType: intent.SaveRecentRoot, SaveRecentRootMetadata: &intent.SaveRecentRootMetadata{TreasuryPool: "t1", PreviousMostRecentRoot: "rr2"}, InitiatorOwnerAccount: "code", State: intent.StateConfirmed}, - {IntentId: "i3", IntentType: intent.SaveRecentRoot, SaveRecentRootMetadata: &intent.SaveRecentRootMetadata{TreasuryPool: "t1", PreviousMostRecentRoot: "rr3"}, InitiatorOwnerAccount: "code", State: intent.StatePending}, - {IntentId: "i4", IntentType: intent.SaveRecentRoot, SaveRecentRootMetadata: &intent.SaveRecentRootMetadata{TreasuryPool: "t2", PreviousMostRecentRoot: "rr4"}, InitiatorOwnerAccount: "code", State: intent.StateConfirmed}, - {IntentId: "i5", IntentType: intent.SaveRecentRoot, SaveRecentRootMetadata: &intent.SaveRecentRootMetadata{TreasuryPool: "t2", PreviousMostRecentRoot: "rr5"}, InitiatorOwnerAccount: "code", State: intent.StateConfirmed}, - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } - - intentRecord, err := s.GetLatestSaveRecentRootIntentForTreasury(ctx, "t1") - require.NoError(t, err) - assert.Equal(t, "i3", intentRecord.IntentId) - assert.Equal(t, "t1", intentRecord.SaveRecentRootMetadata.TreasuryPool) - assert.Equal(t, "rr3", intentRecord.SaveRecentRootMetadata.PreviousMostRecentRoot) - - intentRecord, err = s.GetLatestSaveRecentRootIntentForTreasury(ctx, "t2") - require.NoError(t, err) - assert.Equal(t, "i5", intentRecord.IntentId) - assert.Equal(t, "t2", intentRecord.SaveRecentRootMetadata.TreasuryPool) - assert.Equal(t, "rr5", intentRecord.SaveRecentRootMetadata.PreviousMostRecentRoot) - - _, err = s.GetLatestSaveRecentRootIntentForTreasury(ctx, "t3") - assert.Equal(t, intent.ErrIntentNotFound, err) - }) -} - func testGetOriginalGiftCardIssuedIntent(t *testing.T, s intent.Store) { t.Run("testGetOriginalGiftCardIssuedIntent", func(t *testing.T) { - ctx := context.Background() + /* + ctx := context.Background() - records := []intent.Record{ - {IntentId: "i1", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a1", DestinationOwnerAccount: "o1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + records := []intent.Record{ + {IntentId: "i1", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a1", DestinationOwnerAccount: "o1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i2", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i3", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i4", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i5", IntentType: intent.ExternalDeposit, ExternalDepositMetadata: &intent.ExternalDepositMetadata{DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i6", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "source", Destination: "a2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i2", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i3", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i4", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i5", IntentType: intent.ExternalDeposit, ExternalDepositMetadata: &intent.ExternalDepositMetadata{DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i6", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "source", Destination: "a2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i7", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i8", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i7", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i8", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i9", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, - {IntentId: "i10", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, - } + {IntentId: "i9", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, + {IntentId: "i10", IntentType: intent.SendPrivatePayment, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, + } - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } + for _, record := range records { + require.NoError(t, s.Save(ctx, &record)) + } - _, err := s.GetOriginalGiftCardIssuedIntent(ctx, "unknown") - assert.Equal(t, intent.ErrIntentNotFound, err) + _, err := s.GetOriginalGiftCardIssuedIntent(ctx, "unknown") + assert.Equal(t, intent.ErrIntentNotFound, err) - _, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a1") - assert.Equal(t, intent.ErrIntentNotFound, err) + _, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a1") + assert.Equal(t, intent.ErrIntentNotFound, err) - actual, err := s.GetOriginalGiftCardIssuedIntent(ctx, "a2") - require.NoError(t, err) - assert.Equal(t, "i2", actual.IntentId) + actual, err := s.GetOriginalGiftCardIssuedIntent(ctx, "a2") + require.NoError(t, err) + assert.Equal(t, "i2", actual.IntentId) - _, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a3") - assert.Equal(t, intent.ErrMultilpeIntentsFound, err) + _, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a3") + assert.Equal(t, intent.ErrMultilpeIntentsFound, err) - actual, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a4") - require.NoError(t, err) - assert.Equal(t, "i9", actual.IntentId) + actual, err = s.GetOriginalGiftCardIssuedIntent(ctx, "a4") + require.NoError(t, err) + assert.Equal(t, "i9", actual.IntentId) + */ }) } func testGetGiftCardClaimedIntent(t *testing.T, s intent.Store) { t.Run("testGetGiftCardClaimedIntent", func(t *testing.T) { - ctx := context.Background() - - records := []intent.Record{ - {IntentId: "i1", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a1", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - - {IntentId: "i2", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i3", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i4", IntentType: intent.ReceivePaymentsPrivately, ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{Source: "a2", Quantity: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i5", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a2", Destination: "destination", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - - {IntentId: "i6", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i7", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - - {IntentId: "i8", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, - {IntentId: "i9", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, - } - - for _, record := range records { - require.NoError(t, s.Save(ctx, &record)) - } + /* + ctx := context.Background() - _, err := s.GetGiftCardClaimedIntent(ctx, "unknown") - assert.Equal(t, intent.ErrIntentNotFound, err) + records := []intent.Record{ + {IntentId: "i1", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a1", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - _, err = s.GetGiftCardClaimedIntent(ctx, "a1") - assert.Equal(t, intent.ErrIntentNotFound, err) + {IntentId: "i2", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i3", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i4", IntentType: intent.ReceivePaymentsPrivately, ReceivePaymentsPrivatelyMetadata: &intent.ReceivePaymentsPrivatelyMetadata{Source: "a2", Quantity: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i5", IntentType: intent.LegacyPayment, MoneyTransferMetadata: &intent.MoneyTransferMetadata{Source: "a2", Destination: "destination", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - actual, err := s.GetGiftCardClaimedIntent(ctx, "a2") - require.NoError(t, err) - assert.Equal(t, "i3", actual.IntentId) + {IntentId: "i6", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i7", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - _, err = s.GetGiftCardClaimedIntent(ctx, "a3") - assert.Equal(t, intent.ErrMultilpeIntentsFound, err) + {IntentId: "i8", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, + {IntentId: "i9", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, + } - actual, err = s.GetGiftCardClaimedIntent(ctx, "a4") - require.NoError(t, err) - assert.Equal(t, "i9", actual.IntentId) - }) -} + for _, record := range records { + require.NoError(t, s.Save(ctx, &record)) + } -func testChatPayment(t *testing.T, s intent.Store) { - t.Run("testChatPayment", func(t *testing.T) { - record := &intent.Record{ - IntentId: "i1", - IntentType: intent.SendPrivatePayment, - InitiatorOwnerAccount: "init1", - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - DestinationOwnerAccount: "do", - DestinationTokenAccount: "dt", - Quantity: 1, - ExchangeCurrency: "USD", - ExchangeRate: 1, - NativeAmount: 1, - UsdMarketValue: 1, - IsChat: true, - ChatId: "chatId", - }, - } - require.NoError(t, s.Save(context.Background(), record)) + _, err := s.GetGiftCardClaimedIntent(ctx, "unknown") + assert.Equal(t, intent.ErrIntentNotFound, err) - saved, err := s.Get(context.Background(), record.IntentId) - require.NoError(t, err) - require.Equal(t, record, saved) - }) + _, err = s.GetGiftCardClaimedIntent(ctx, "a1") + assert.Equal(t, intent.ErrIntentNotFound, err) - t.Run("testChatPayment invalid", func(t *testing.T) { - base := &intent.Record{ - IntentId: "i1", - IntentType: intent.SendPrivatePayment, - InitiatorOwnerAccount: "init1", - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - DestinationOwnerAccount: "do", - DestinationTokenAccount: "dt", - Quantity: 1, - ExchangeCurrency: "USD", - ExchangeRate: 1, - NativeAmount: 1, - UsdMarketValue: 1, - }, - } + actual, err := s.GetGiftCardClaimedIntent(ctx, "a2") + require.NoError(t, err) + assert.Equal(t, "i3", actual.IntentId) - r := base.Clone() - r.SendPrivatePaymentMetadata.IsChat = true - require.Error(t, s.Save(context.Background(), &r)) + _, err = s.GetGiftCardClaimedIntent(ctx, "a3") + assert.Equal(t, intent.ErrMultilpeIntentsFound, err) - r = base.Clone() - r.SendPrivatePaymentMetadata.ChatId = "chatId" - require.Error(t, s.Save(context.Background(), &r)) + actual, err = s.GetGiftCardClaimedIntent(ctx, "a4") + require.NoError(t, err) + assert.Equal(t, "i9", actual.IntentId) + */ }) } diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 616bb662..af676284 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -9,8 +9,6 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - "golang.org/x/text/language" "github.com/code-payments/code-server/pkg/cache" currency_lib "github.com/code-payments/code-server/pkg/currency" @@ -23,110 +21,61 @@ import ( "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/airdrop" - "github.com/code-payments/code-server/pkg/code/data/badgecount" "github.com/code-payments/code-server/pkg/code/data/balance" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/contact" "github.com/code-payments/code-server/pkg/code/data/currency" cvm_ram "github.com/code-payments/code-server/pkg/code/data/cvm/ram" cvm_storage "github.com/code-payments/code-server/pkg/code/data/cvm/storage" "github.com/code-payments/code-server/pkg/code/data/deposit" - "github.com/code-payments/code-server/pkg/code/data/event" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/login" "github.com/code-payments/code-server/pkg/code/data/merkletree" + "github.com/code-payments/code-server/pkg/code/data/messaging" "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/onramp" - "github.com/code-payments/code-server/pkg/code/data/payment" "github.com/code-payments/code-server/pkg/code/data/paymentrequest" - "github.com/code-payments/code-server/pkg/code/data/paywall" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/push" "github.com/code-payments/code-server/pkg/code/data/rendezvous" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/code/data/twitter" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/code/data/user/storage" "github.com/code-payments/code-server/pkg/code/data/vault" "github.com/code-payments/code-server/pkg/code/data/webhook" account_memory_client "github.com/code-payments/code-server/pkg/code/data/account/memory" action_memory_client "github.com/code-payments/code-server/pkg/code/data/action/memory" - airdrop_memory_client "github.com/code-payments/code-server/pkg/code/data/airdrop/memory" - badgecount_memory_client "github.com/code-payments/code-server/pkg/code/data/badgecount/memory" balance_memory_client "github.com/code-payments/code-server/pkg/code/data/balance/memory" - chat_memory_client "github.com/code-payments/code-server/pkg/code/data/chat/memory" - commitment_memory_client "github.com/code-payments/code-server/pkg/code/data/commitment/memory" - contact_memory_client "github.com/code-payments/code-server/pkg/code/data/contact/memory" currency_memory_client "github.com/code-payments/code-server/pkg/code/data/currency/memory" cvm_ram_memory_client "github.com/code-payments/code-server/pkg/code/data/cvm/ram/memory" cvm_storage_memory_client "github.com/code-payments/code-server/pkg/code/data/cvm/storage/memory" deposit_memory_client "github.com/code-payments/code-server/pkg/code/data/deposit/memory" - event_memory_client "github.com/code-payments/code-server/pkg/code/data/event/memory" fulfillment_memory_client "github.com/code-payments/code-server/pkg/code/data/fulfillment/memory" intent_memory_client "github.com/code-payments/code-server/pkg/code/data/intent/memory" - login_memory_client "github.com/code-payments/code-server/pkg/code/data/login/memory" merkletree_memory_client "github.com/code-payments/code-server/pkg/code/data/merkletree/memory" - messaging "github.com/code-payments/code-server/pkg/code/data/messaging" messaging_memory_client "github.com/code-payments/code-server/pkg/code/data/messaging/memory" nonce_memory_client "github.com/code-payments/code-server/pkg/code/data/nonce/memory" onramp_memory_client "github.com/code-payments/code-server/pkg/code/data/onramp/memory" - payment_memory_client "github.com/code-payments/code-server/pkg/code/data/payment/memory" paymentrequest_memory_client "github.com/code-payments/code-server/pkg/code/data/paymentrequest/memory" - paywall_memory_client "github.com/code-payments/code-server/pkg/code/data/paywall/memory" - phone_memory_client "github.com/code-payments/code-server/pkg/code/data/phone/memory" - preferences_memory_client "github.com/code-payments/code-server/pkg/code/data/preferences/memory" - push_memory_client "github.com/code-payments/code-server/pkg/code/data/push/memory" rendezvous_memory_client "github.com/code-payments/code-server/pkg/code/data/rendezvous/memory" timelock_memory_client "github.com/code-payments/code-server/pkg/code/data/timelock/memory" transaction_memory_client "github.com/code-payments/code-server/pkg/code/data/transaction/memory" - treasury_memory_client "github.com/code-payments/code-server/pkg/code/data/treasury/memory" - twitter_memory_client "github.com/code-payments/code-server/pkg/code/data/twitter/memory" - user_identity_memory_client "github.com/code-payments/code-server/pkg/code/data/user/identity/memory" - user_storage_memory_client "github.com/code-payments/code-server/pkg/code/data/user/storage/memory" vault_memory_client "github.com/code-payments/code-server/pkg/code/data/vault/memory" webhook_memory_client "github.com/code-payments/code-server/pkg/code/data/webhook/memory" account_postgres_client "github.com/code-payments/code-server/pkg/code/data/account/postgres" action_postgres_client "github.com/code-payments/code-server/pkg/code/data/action/postgres" - airdrop_postgres_client "github.com/code-payments/code-server/pkg/code/data/airdrop/postgres" - badgecount_postgres_client "github.com/code-payments/code-server/pkg/code/data/badgecount/postgres" balance_postgres_client "github.com/code-payments/code-server/pkg/code/data/balance/postgres" - chat_postgres_client "github.com/code-payments/code-server/pkg/code/data/chat/postgres" - commitment_postgres_client "github.com/code-payments/code-server/pkg/code/data/commitment/postgres" - contact_postgres_client "github.com/code-payments/code-server/pkg/code/data/contact/postgres" currency_postgres_client "github.com/code-payments/code-server/pkg/code/data/currency/postgres" cvm_ram_postgres_client "github.com/code-payments/code-server/pkg/code/data/cvm/ram/postgres" cvm_storage_postgres_client "github.com/code-payments/code-server/pkg/code/data/cvm/storage/postgres" deposit_postgres_client "github.com/code-payments/code-server/pkg/code/data/deposit/postgres" - event_postgres_client "github.com/code-payments/code-server/pkg/code/data/event/postgres" fulfillment_postgres_client "github.com/code-payments/code-server/pkg/code/data/fulfillment/postgres" intent_postgres_client "github.com/code-payments/code-server/pkg/code/data/intent/postgres" - login_postgres_client "github.com/code-payments/code-server/pkg/code/data/login/postgres" merkletree_postgres_client "github.com/code-payments/code-server/pkg/code/data/merkletree/postgres" messaging_postgres_client "github.com/code-payments/code-server/pkg/code/data/messaging/postgres" nonce_postgres_client "github.com/code-payments/code-server/pkg/code/data/nonce/postgres" onramp_postgres_client "github.com/code-payments/code-server/pkg/code/data/onramp/postgres" - payment_postgres_client "github.com/code-payments/code-server/pkg/code/data/payment/postgres" paymentrequest_postgres_client "github.com/code-payments/code-server/pkg/code/data/paymentrequest/postgres" - paywall_postgres_client "github.com/code-payments/code-server/pkg/code/data/paywall/postgres" - phone_postgres_client "github.com/code-payments/code-server/pkg/code/data/phone/postgres" - preferences_postgres_client "github.com/code-payments/code-server/pkg/code/data/preferences/postgres" - push_postgres_client "github.com/code-payments/code-server/pkg/code/data/push/postgres" rendezvous_postgres_client "github.com/code-payments/code-server/pkg/code/data/rendezvous/postgres" timelock_postgres_client "github.com/code-payments/code-server/pkg/code/data/timelock/postgres" transaction_postgres_client "github.com/code-payments/code-server/pkg/code/data/transaction/postgres" - treasury_postgres_client "github.com/code-payments/code-server/pkg/code/data/treasury/postgres" - twitter_postgres_client "github.com/code-payments/code-server/pkg/code/data/twitter/postgres" - user_identity_postgres_client "github.com/code-payments/code-server/pkg/code/data/user/identity/postgres" - user_storage_postgres_client "github.com/code-payments/code-server/pkg/code/data/user/storage/postgres" vault_postgres_client "github.com/code-payments/code-server/pkg/code/data/vault/postgres" webhook_postgres_client "github.com/code-payments/code-server/pkg/code/data/webhook/postgres" ) @@ -216,7 +165,6 @@ type DatabaseData interface { PutAllFulfillments(ctx context.Context, records ...*fulfillment.Record) error UpdateFulfillment(ctx context.Context, record *fulfillment.Record) error MarkFulfillmentAsActivelyScheduled(ctx context.Context, id uint64) error - ActivelyScheduleTreasuryAdvanceFulfillments(ctx context.Context, treasury string, intentOrderingIndex uint64, limit int) (uint64, error) // Intent // -------------------------------------------------------------------------------- @@ -224,13 +172,6 @@ type DatabaseData interface { GetIntent(ctx context.Context, intentID string) (*intent.Record, error) GetIntentBySignature(ctx context.Context, signature string) (*intent.Record, error) GetLatestIntentByInitiatorAndType(ctx context.Context, intentType intent.Type, owner string) (*intent.Record, error) - GetAllIntentsByOwner(ctx context.Context, owner string, opts ...query.Option) ([]*intent.Record, error) - GetIntentCountForAntispam(ctx context.Context, intentType intent.Type, phoneNumber string, states []intent.State, since time.Time) (uint64, error) - GetIntentCountWithOwnerInteractionsForAntispam(ctx context.Context, sourceOwner, destinationOwner string, states []intent.State, since time.Time) (uint64, error) - GetTransactedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) - GetDepositedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) - GetNetBalanceFromPrePrivacy2022Intents(ctx context.Context, account string) (int64, error) - GetLatestSaveRecentRootIntentForTreasury(ctx context.Context, treasury string) (*intent.Record, error) GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) GetGiftCardClaimedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) @@ -246,15 +187,6 @@ type DatabaseData interface { GetGiftCardClaimedAction(ctx context.Context, giftCardVault string) (*action.Record, error) GetGiftCardAutoReturnAction(ctx context.Context, giftCardVault string) (*action.Record, error) - // Payment (mostly deprecated for legacy accounts and unmigrated processes) - // -------------------------------------------------------------------------------- - GetPayment(ctx context.Context, sig string, index int) (*payment.Record, error) - CreatePayment(ctx context.Context, record *payment.Record) error - UpdateOrCreatePayment(ctx context.Context, record *payment.Record) error - GetPaymentHistory(ctx context.Context, account string, opts ...query.Option) ([]*payment.Record, error) - GetPaymentHistoryWithinBlockRange(ctx context.Context, account string, lowerBound, upperBound uint64, opts ...query.Option) ([]*payment.Record, error) - GetLegacyTotalExternalDepositAmountFromPrePrivacy2022Accounts(ctx context.Context, account string) (uint64, error) - // Transaction // -------------------------------------------------------------------------------- GetTransaction(ctx context.Context, sig string) (*transaction.Record, error) @@ -266,45 +198,6 @@ type DatabaseData interface { GetMessages(ctx context.Context, account string) ([]*messaging.Record, error) DeleteMessage(ctx context.Context, account string, messageID uuid.UUID) error - // Phone - // -------------------------------------------------------------------------------- - SavePhoneVerification(ctx context.Context, v *phone.Verification) error - GetPhoneVerification(ctx context.Context, account, phoneNumber string) (*phone.Verification, error) - GetLatestPhoneVerificationForAccount(ctx context.Context, account string) (*phone.Verification, error) - GetLatestPhoneVerificationForNumber(ctx context.Context, phoneNumber string) (*phone.Verification, error) - GetAllPhoneVerificationsForNumber(ctx context.Context, phoneNumber string) ([]*phone.Verification, error) - SavePhoneLinkingToken(ctx context.Context, token *phone.LinkingToken) error - UsePhoneLinkingToken(ctx context.Context, phoneNumber, code string) error - FilterVerifiedPhoneNumbers(ctx context.Context, phoneNumbers []string) ([]string, error) - SaveOwnerAccountPhoneSetting(ctx context.Context, phoneNumber string, newSettings *phone.OwnerAccountSetting) error - IsPhoneNumberLinkedToAccount(ctx context.Context, phoneNumber string, tokenAccount string) (bool, error) - IsPhoneNumberEnabledForRemoteSendToAccount(ctx context.Context, phoneNumber string, tokenAccount string) (bool, error) - PutPhoneEvent(ctx context.Context, event *phone.Event) error - GetLatestPhoneEventForNumberByType(ctx context.Context, phoneNumber string, eventType phone.EventType) (*phone.Event, error) - GetPhoneEventCountForVerificationByType(ctx context.Context, verification string, eventType phone.EventType) (uint64, error) - GetPhoneEventCountForNumberByTypeSinceTimestamp(ctx context.Context, phoneNumber string, eventType phone.EventType, since time.Time) (uint64, error) - GetUniquePhoneVerificationIdCountForNumberSinceTimestamp(ctx context.Context, phoneNumber string, since time.Time) (uint64, error) - - // Contact - // -------------------------------------------------------------------------------- - AddContact(ctx context.Context, owner *user.DataContainerID, contact string) error - BatchAddContacts(ctx context.Context, owner *user.DataContainerID, contacts []string) error - RemoveContact(ctx context.Context, owner *user.DataContainerID, contact string) error - BatchRemoveContacts(ctx context.Context, owner *user.DataContainerID, contacts []string) error - GetContacts(ctx context.Context, owner *user.DataContainerID, limit uint32, pageToken []byte) (contacts []string, nextPageToken []byte, err error) - - // User Identity - // -------------------------------------------------------------------------------- - PutUser(ctx context.Context, record *identity.Record) error - GetUserByID(ctx context.Context, id *user.UserID) (*identity.Record, error) - GetUserByPhoneView(ctx context.Context, phoneNumber string) (*identity.Record, error) - - // User Storage Management - // -------------------------------------------------------------------------------- - PutUserDataContainer(ctx context.Context, container *storage.Record) error - GetUserDataContainerByID(ctx context.Context, id *user.DataContainerID) (*storage.Record, error) - GetUserDataContainerByPhone(ctx context.Context, tokenAccount, phoneNumber string) (*storage.Record, error) - // Timelock // -------------------------------------------------------------------------------- SaveTimelock(ctx context.Context, record *timelock.Record) error @@ -314,36 +207,6 @@ type DatabaseData interface { GetAllTimelocksByState(ctx context.Context, state timelock_token.TimelockState, opts ...query.Option) ([]*timelock.Record, error) GetTimelockCountByState(ctx context.Context, state timelock_token.TimelockState) (uint64, error) - // Push - // -------------------------------------------------------------------------------- - PutPushToken(ctx context.Context, record *push.Record) error - MarkPushTokenAsInvalid(ctx context.Context, pushToken string) error - DeletePushToken(ctx context.Context, pushToken string) error - GetAllValidPushTokensdByDataContainer(ctx context.Context, id *user.DataContainerID) ([]*push.Record, error) - - // Commitment - // -------------------------------------------------------------------------------- - SaveCommitment(ctx context.Context, record *commitment.Record) error - GetCommitmentByAddress(ctx context.Context, address string) (*commitment.Record, error) - GetCommitmentByVault(ctx context.Context, vault string) (*commitment.Record, error) - GetCommitmentByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) - GetAllCommitmentsByState(ctx context.Context, state commitment.State, opts ...query.Option) ([]*commitment.Record, error) - GetUpgradeableCommitmentsByOwner(ctx context.Context, owner string, limit uint64) ([]*commitment.Record, error) - GetUsedTreasuryPoolDeficitFromCommitments(ctx context.Context, treasuryPool string) (uint64, error) - GetTotalTreasuryPoolDeficitFromCommitments(ctx context.Context, treasuryPool string) (uint64, error) - CountCommitmentsByState(ctx context.Context, state commitment.State) (uint64, error) - CountPendingCommitmentRepaymentsDivertedToCommitment(ctx context.Context, address string) (uint64, error) - - // Treasury Pool - // -------------------------------------------------------------------------------- - SaveTreasuryPool(ctx context.Context, record *treasury.Record) error - GetTreasuryPoolByName(ctx context.Context, name string) (*treasury.Record, error) - GetTreasuryPoolByAddress(ctx context.Context, address string) (*treasury.Record, error) - GetTreasuryPoolByVault(ctx context.Context, vault string) (*treasury.Record, error) - GetAllTreasuryPoolsByState(ctx context.Context, state treasury.TreasuryPoolState, opts ...query.Option) ([]*treasury.Record, error) - SaveTreasuryPoolFunding(ctx context.Context, record *treasury.FundingHistoryRecord) error - GetTotalAvailableTreasuryPoolFunds(ctx context.Context, vault string) (uint64, error) - // Merkle Tree // -------------------------------------------------------------------------------- InitializeNewMerkleTree(ctx context.Context, name string, levels uint8, seeds []merkletree.Seed, readOnly bool) (*merkletree.MerkleTree, error) @@ -368,16 +231,6 @@ type DatabaseData interface { CreateRequest(ctx context.Context, record *paymentrequest.Record) error GetRequest(ctx context.Context, intentId string) (*paymentrequest.Record, error) - // Paywall - // -------------------------------------------------------------------------------- - CreatePaywall(ctx context.Context, record *paywall.Record) error - GetPaywallByShortPath(ctx context.Context, path string) (*paywall.Record, error) - - // Event - // -------------------------------------------------------------------------------- - SaveEvent(ctx context.Context, record *event.Record) error - GetEvent(ctx context.Context, id string) (*event.Record, error) - // Webhook // -------------------------------------------------------------------------------- CreateWebhook(ctx context.Context, record *webhook.Record) error @@ -386,32 +239,6 @@ type DatabaseData interface { CountWebhookByState(ctx context.Context, state webhook.State) (uint64, error) GetAllPendingWebhooksReadyToSend(ctx context.Context, limit uint64) ([]*webhook.Record, error) - // Chat - // -------------------------------------------------------------------------------- - PutChat(ctx context.Context, record *chat.Chat) error - GetChatById(ctx context.Context, chatId chat.ChatId) (*chat.Chat, error) - GetAllChatsForUser(ctx context.Context, user string, opts ...query.Option) ([]*chat.Chat, error) - PutChatMessage(ctx context.Context, record *chat.Message) error - DeleteChatMessage(ctx context.Context, chatId chat.ChatId, messageId string) error - GetChatMessage(ctx context.Context, chatId chat.ChatId, messageId string) (*chat.Message, error) - GetAllChatMessages(ctx context.Context, chatId chat.ChatId, opts ...query.Option) ([]*chat.Message, error) - AdvanceChatPointer(ctx context.Context, chatId chat.ChatId, pointer string) error - GetChatUnreadCount(ctx context.Context, chatId chat.ChatId) (uint32, error) - SetChatMuteState(ctx context.Context, chatId chat.ChatId, isMuted bool) error - SetChatSubscriptionState(ctx context.Context, chatId chat.ChatId, isSubscribed bool) error - - // Badge Count - // -------------------------------------------------------------------------------- - AddToBadgeCount(ctx context.Context, owner string, amount uint32) error - ResetBadgeCount(ctx context.Context, owner string) error - GetBadgeCount(ctx context.Context, owner string) (*badgecount.Record, error) - - // Login - // -------------------------------------------------------------------------------- - SaveLogins(ctx context.Context, record *login.MultiRecord) error - GetLoginsByAppInstall(ctx context.Context, appInstallId string) (*login.MultiRecord, error) - GetLatestLoginByOwner(ctx context.Context, owner string) (*login.Record, error) - // Balance // -------------------------------------------------------------------------------- SaveBalanceCheckpoint(ctx context.Context, record *balance.Record) error @@ -422,27 +249,6 @@ type DatabaseData interface { PutFiatOnrampPurchase(ctx context.Context, record *onramp.Record) error GetFiatOnrampPurchase(ctx context.Context, nonce uuid.UUID) (*onramp.Record, error) - // User Preferences - // -------------------------------------------------------------------------------- - SaveUserPreferences(ctx context.Context, record *preferences.Record) error - GetUserPreferences(ctx context.Context, id *user.DataContainerID) (*preferences.Record, error) - GetUserLocale(ctx context.Context, owner string) (language.Tag, error) - - // Airdrop - // -------------------------------------------------------------------------------- - MarkIneligibleForAirdrop(ctx context.Context, owner string) error - IsEligibleForAirdrop(ctx context.Context, owner string) (bool, error) - - // Twitter - // -------------------------------------------------------------------------------- - SaveTwitterUser(ctx context.Context, record *twitter.Record) error - GetTwitterUserByUsername(ctx context.Context, username string) (*twitter.Record, error) - GetTwitterUserByTipAddress(ctx context.Context, tipAddress string) (*twitter.Record, error) - GetStaleTwitterUsers(ctx context.Context, minAge time.Duration, limit int) ([]*twitter.Record, error) - MarkTweetAsProcessed(ctx context.Context, tweetId string) error - IsTweetProcessed(ctx context.Context, tweetId string) (bool, error) - MarkTwitterNonceAsUsed(ctx context.Context, tweetId string, nonce uuid.UUID) error - // CVM RAM // -------------------------------------------------------------------------------- InitializeVmMemory(ctx context.Context, record *cvm_ram.Record) error @@ -473,32 +279,16 @@ type DatabaseProvider struct { fulfillments fulfillment.Store intents intent.Store actions action.Store - payments payment.Store transactions transaction.Store messages messaging.Store - phone phone.Store - contact contact.Store - userIdentity identity.Store - userStorage storage.Store timelock timelock.Store - push push.Store - commitment commitment.Store - treasury treasury.Store merkleTree merkletree.Store deposits deposit.Store rendezvous rendezvous.Store paymentRequest paymentrequest.Store - paywall paywall.Store - event event.Store webhook webhook.Store - chat chat.Store - badgecount badgecount.Store - login login.Store balance balance.Store onramp onramp.Store - preferences preferences.Store - airdrop airdrop.Store - twitter twitter.Store cvmRam cvm_ram.Store cvmStorage cvm_storage.Store @@ -536,33 +326,17 @@ func NewDatabaseProvider(dbConfig *pg.Config) (DatabaseData, error) { fulfillments: fulfillment_postgres_client.New(db), intents: intent_postgres_client.New(db), actions: action_postgres_client.New(db), - payments: payment_postgres_client.New(db), transactions: transaction_postgres_client.New(db), messages: messaging_postgres_client.New(db), - phone: phone_postgres_client.New(db), - contact: contact_postgres_client.New(db), - userIdentity: user_identity_postgres_client.New(db), - userStorage: user_storage_postgres_client.New(db), timelock: timelock_postgres_client.New(db), vault: vault_postgres_client.New(db), - push: push_postgres_client.New(db), - commitment: commitment_postgres_client.New(db), - treasury: treasury_postgres_client.New(db), merkleTree: merkletree_postgres_client.New(db), deposits: deposit_postgres_client.New(db), rendezvous: rendezvous_postgres_client.New(db), paymentRequest: paymentrequest_postgres_client.New(db), - paywall: paywall_postgres_client.New(db), - event: event_postgres_client.New(db), webhook: webhook_postgres_client.New(db), - chat: chat_postgres_client.New(db), - badgecount: badgecount_postgres_client.New(db), - login: login_postgres_client.New(db), balance: balance_postgres_client.New(db), onramp: onramp_postgres_client.New(db), - preferences: preferences_postgres_client.New(db), - airdrop: airdrop_postgres_client.New(db), - twitter: twitter_postgres_client.New(db), cvmRam: cvm_ram_postgres_client.New(db), cvmStorage: cvm_storage_postgres_client.New(db), @@ -581,33 +355,17 @@ func NewTestDatabaseProvider() DatabaseData { fulfillments: fulfillment_memory_client.New(), intents: intent_memory_client.New(), actions: action_memory_client.New(), - payments: payment_memory_client.New(), transactions: transaction_memory_client.New(), - phone: phone_memory_client.New(), - contact: contact_memory_client.New(), - userIdentity: user_identity_memory_client.New(), - userStorage: user_storage_memory_client.New(), timelock: timelock_memory_client.New(), vault: vault_memory_client.New(), - push: push_memory_client.New(), - commitment: commitment_memory_client.New(), - treasury: treasury_memory_client.New(), merkleTree: merkletree_memory_client.New(), messages: messaging_memory_client.New(), deposits: deposit_memory_client.New(), rendezvous: rendezvous_memory_client.New(), paymentRequest: paymentrequest_memory_client.New(), - paywall: paywall_memory_client.New(), - event: event_memory_client.New(), webhook: webhook_memory_client.New(), - chat: chat_memory_client.New(), - badgecount: badgecount_memory_client.New(), - login: login_memory_client.New(), balance: balance_memory_client.New(), onramp: onramp_memory_client.New(), - preferences: preferences_memory_client.New(), - airdrop: airdrop_memory_client.New(), - twitter: twitter_memory_client.New(), cvmRam: cvm_ram_memory_client.New(), cvmStorage: cvm_storage_memory_client.New(), @@ -861,9 +619,6 @@ func (dp *DatabaseProvider) UpdateFulfillment(ctx context.Context, record *fulfi func (dp *DatabaseProvider) MarkFulfillmentAsActivelyScheduled(ctx context.Context, id uint64) error { return dp.fulfillments.MarkAsActivelyScheduled(ctx, id) } -func (dp *DatabaseProvider) ActivelyScheduleTreasuryAdvanceFulfillments(ctx context.Context, treasury string, intentOrderingIndex uint64, limit int) (uint64, error) { - return dp.fulfillments.ActivelyScheduleTreasuryAdvances(ctx, treasury, intentOrderingIndex, limit) -} // Intent // -------------------------------------------------------------------------------- @@ -883,32 +638,6 @@ func (dp *DatabaseProvider) GetIntentBySignature(ctx context.Context, signature func (dp *DatabaseProvider) GetLatestIntentByInitiatorAndType(ctx context.Context, intentType intent.Type, owner string) (*intent.Record, error) { return dp.intents.GetLatestByInitiatorAndType(ctx, intentType, owner) } -func (dp *DatabaseProvider) GetAllIntentsByOwner(ctx context.Context, owner string, opts ...query.Option) ([]*intent.Record, error) { - req, err := query.DefaultPaginationHandler(opts...) - if err != nil { - return nil, err - } - - return dp.intents.GetAllByOwner(ctx, owner, req.Cursor, req.Limit, req.SortBy) -} -func (dp *DatabaseProvider) GetIntentCountForAntispam(ctx context.Context, intentType intent.Type, phoneNumber string, states []intent.State, since time.Time) (uint64, error) { - return dp.intents.CountForAntispam(ctx, intentType, phoneNumber, states, since) -} -func (dp *DatabaseProvider) GetIntentCountWithOwnerInteractionsForAntispam(ctx context.Context, sourceOwner, destinationOwner string, states []intent.State, since time.Time) (uint64, error) { - return dp.intents.CountOwnerInteractionsForAntispam(ctx, sourceOwner, destinationOwner, states, since) -} -func (dp *DatabaseProvider) GetTransactedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) { - return dp.intents.GetTransactedAmountForAntiMoneyLaundering(ctx, phoneNumber, since) -} -func (dp *DatabaseProvider) GetDepositedAmountForAntiMoneyLaundering(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) { - return dp.intents.GetDepositedAmountForAntiMoneyLaundering(ctx, phoneNumber, since) -} -func (dp *DatabaseProvider) GetNetBalanceFromPrePrivacy2022Intents(ctx context.Context, account string) (int64, error) { - return dp.intents.GetNetBalanceFromPrePrivacy2022Intents(ctx, account) -} -func (dp *DatabaseProvider) GetLatestSaveRecentRootIntentForTreasury(ctx context.Context, treasury string) (*intent.Record, error) { - return dp.intents.GetLatestSaveRecentRootIntentForTreasury(ctx, treasury) -} func (dp *DatabaseProvider) GetOriginalGiftCardIssuedIntent(ctx context.Context, giftCardVault string) (*intent.Record, error) { return dp.intents.GetOriginalGiftCardIssuedIntent(ctx, giftCardVault) } @@ -949,77 +678,6 @@ func (dp *DatabaseProvider) GetGiftCardAutoReturnAction(ctx context.Context, gif return dp.actions.GetGiftCardAutoReturnAction(ctx, giftCardVault) } -// Payment -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) GetPayment(ctx context.Context, sig string, index int) (*payment.Record, error) { - return dp.payments.Get(ctx, sig, uint32(index)) -} -func (dp *DatabaseProvider) CreatePayment(ctx context.Context, record *payment.Record) error { - return dp.payments.Put(ctx, record) -} -func (dp *DatabaseProvider) UpdatePayment(ctx context.Context, record *payment.Record) error { - return dp.payments.Update(ctx, record) -} -func (dp *DatabaseProvider) UpdateOrCreatePayment(ctx context.Context, record *payment.Record) error { - if record.Id > 0 { - return dp.UpdatePayment(ctx, record) - } - return dp.CreatePayment(ctx, record) -} -func (dp *DatabaseProvider) GetPaymentHistory(ctx context.Context, account string, opts ...query.Option) ([]*payment.Record, error) { - req := query.QueryOptions{ - Limit: maxPaymentHistoryReqSize, - SortBy: query.Ascending, - Supported: query.CanLimitResults | query.CanSortBy | query.CanQueryByCursor | query.CanFilterBy, - } - req.Apply(opts...) - - if req.Limit > maxPaymentHistoryReqSize { - return nil, query.ErrQueryNotSupported - } - - var cursor uint64 - if len(req.Cursor) > 0 { - cursor = query.FromCursor(req.Cursor) - } else { - cursor = 0 - } - - if req.FilterBy.Valid { - return dp.payments.GetAllForAccountByType(ctx, account, cursor, uint(req.Limit), req.SortBy, payment.PaymentType(req.FilterBy.Value)) - } - - return dp.payments.GetAllForAccount(ctx, account, cursor, uint(req.Limit), req.SortBy) -} -func (dp *DatabaseProvider) GetPaymentHistoryWithinBlockRange(ctx context.Context, account string, lowerBound, upperBound uint64, opts ...query.Option) ([]*payment.Record, error) { - req := query.QueryOptions{ - Limit: maxPaymentHistoryReqSize, - SortBy: query.Ascending, - Supported: query.CanLimitResults | query.CanSortBy | query.CanQueryByCursor | query.CanFilterBy, - } - req.Apply(opts...) - - if req.Limit > maxPaymentHistoryReqSize { - return nil, query.ErrQueryNotSupported - } - - var cursor uint64 - if len(req.Cursor) > 0 { - cursor = query.FromCursor(req.Cursor) - } else { - cursor = 0 - } - - if req.FilterBy.Valid { - return dp.payments.GetAllForAccountByTypeWithinBlockRange(ctx, account, lowerBound, upperBound, cursor, uint(req.Limit), req.SortBy, payment.PaymentType(req.FilterBy.Value)) - } - - return nil, query.ErrQueryNotSupported -} -func (dp *DatabaseProvider) GetLegacyTotalExternalDepositAmountFromPrePrivacy2022Accounts(ctx context.Context, account string) (uint64, error) { - return dp.payments.GetExternalDepositAmount(ctx, account) -} - // Transaction // -------------------------------------------------------------------------------- func (dp *DatabaseProvider) GetTransaction(ctx context.Context, sig string) (*transaction.Record, error) { @@ -1043,127 +701,6 @@ func (dp *DatabaseProvider) DeleteMessage(ctx context.Context, account string, m return dp.messages.Delete(ctx, account, messageID) } -// Phone -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) SavePhoneVerification(ctx context.Context, v *phone.Verification) error { - return dp.phone.SaveVerification(ctx, v) -} -func (dp *DatabaseProvider) GetPhoneVerification(ctx context.Context, account, phoneNumber string) (*phone.Verification, error) { - return dp.phone.GetVerification(ctx, account, phoneNumber) -} -func (dp *DatabaseProvider) GetLatestPhoneVerificationForAccount(ctx context.Context, account string) (*phone.Verification, error) { - return dp.phone.GetLatestVerificationForAccount(ctx, account) -} -func (dp *DatabaseProvider) GetLatestPhoneVerificationForNumber(ctx context.Context, phoneNumber string) (*phone.Verification, error) { - return dp.phone.GetLatestVerificationForNumber(ctx, phoneNumber) -} -func (dp *DatabaseProvider) GetAllPhoneVerificationsForNumber(ctx context.Context, phoneNumber string) ([]*phone.Verification, error) { - return dp.phone.GetAllVerificationsForNumber(ctx, phoneNumber) -} -func (dp *DatabaseProvider) SavePhoneLinkingToken(ctx context.Context, token *phone.LinkingToken) error { - return dp.phone.SaveLinkingToken(ctx, token) -} -func (dp *DatabaseProvider) UsePhoneLinkingToken(ctx context.Context, phoneNumber, code string) error { - return dp.phone.UseLinkingToken(ctx, phoneNumber, code) -} -func (dp *DatabaseProvider) FilterVerifiedPhoneNumbers(ctx context.Context, phoneNumbers []string) ([]string, error) { - return dp.phone.FilterVerifiedNumbers(ctx, phoneNumbers) -} -func (dp *DatabaseProvider) SaveOwnerAccountPhoneSetting(ctx context.Context, phoneNumber string, newSettings *phone.OwnerAccountSetting) error { - return dp.phone.SaveOwnerAccountSetting(ctx, phoneNumber, newSettings) -} -func (dp *DatabaseProvider) IsPhoneNumberLinkedToAccount(ctx context.Context, phoneNumber string, ownerAccount string) (bool, error) { - verification, err := dp.GetLatestPhoneVerificationForNumber(ctx, phoneNumber) - if err != nil { - return false, err - } else if verification.OwnerAccount != ownerAccount { - return false, nil - } - - phoneSettings, err := dp.phone.GetSettings(ctx, phoneNumber) - if err != nil { - return false, err - } - - tokenAccountSettings, ok := phoneSettings.ByOwnerAccount[ownerAccount] - if !ok { - return true, nil - } - - if tokenAccountSettings.IsUnlinked == nil { - return true, nil - } - return !*tokenAccountSettings.IsUnlinked, nil -} -func (dp *DatabaseProvider) IsPhoneNumberEnabledForRemoteSendToAccount(ctx context.Context, phoneNumber string, ownerAccount string) (bool, error) { - // These are equivalent at the time of writing this - return dp.IsPhoneNumberLinkedToAccount(ctx, phoneNumber, ownerAccount) -} -func (dp *DatabaseProvider) PutPhoneEvent(ctx context.Context, event *phone.Event) error { - return dp.phone.PutEvent(ctx, event) -} -func (dp *DatabaseProvider) GetLatestPhoneEventForNumberByType(ctx context.Context, phoneNumber string, eventType phone.EventType) (*phone.Event, error) { - return dp.phone.GetLatestEventForNumberByType(ctx, phoneNumber, eventType) -} -func (dp *DatabaseProvider) GetPhoneEventCountForVerificationByType(ctx context.Context, verification string, eventType phone.EventType) (uint64, error) { - return dp.phone.CountEventsForVerificationByType(ctx, verification, eventType) -} -func (dp *DatabaseProvider) GetPhoneEventCountForNumberByTypeSinceTimestamp(ctx context.Context, phoneNumber string, eventType phone.EventType, since time.Time) (uint64, error) { - return dp.phone.CountEventsForNumberByTypeSinceTimestamp(ctx, phoneNumber, eventType, since) -} -func (dp *DatabaseProvider) GetUniquePhoneVerificationIdCountForNumberSinceTimestamp(ctx context.Context, phoneNumber string, since time.Time) (uint64, error) { - return dp.phone.CountUniqueVerificationIdsForNumberSinceTimestamp(ctx, phoneNumber, since) -} - -// Contact -// -------------------------------------------------------------------------------- - -func (dp *DatabaseProvider) AddContact(ctx context.Context, owner *user.DataContainerID, contact string) error { - return dp.contact.Add(ctx, owner, contact) -} -func (dp *DatabaseProvider) BatchAddContacts(ctx context.Context, owner *user.DataContainerID, contacts []string) error { - return dp.contact.BatchAdd(ctx, owner, contacts) -} -func (dp *DatabaseProvider) RemoveContact(ctx context.Context, owner *user.DataContainerID, contact string) error { - return dp.contact.Remove(ctx, owner, contact) -} -func (dp *DatabaseProvider) GetContacts(ctx context.Context, owner *user.DataContainerID, limit uint32, pageToken []byte) (contacts []string, nextPageToken []byte, err error) { - return dp.contact.Get(ctx, owner, limit, pageToken) -} -func (dp *DatabaseProvider) BatchRemoveContacts(ctx context.Context, owner *user.DataContainerID, contacts []string) error { - return dp.contact.BatchRemove(ctx, owner, contacts) -} - -// User Identity -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) PutUser(ctx context.Context, record *identity.Record) error { - return dp.userIdentity.Put(ctx, record) -} -func (dp *DatabaseProvider) GetUserByID(ctx context.Context, id *user.UserID) (*identity.Record, error) { - return dp.userIdentity.GetByID(ctx, id) -} -func (dp *DatabaseProvider) GetUserByPhoneView(ctx context.Context, phoneNumber string) (*identity.Record, error) { - view := &user.View{ - PhoneNumber: &phoneNumber, - } - return dp.userIdentity.GetByView(ctx, view) -} - -// User Storage Management -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) PutUserDataContainer(ctx context.Context, record *storage.Record) error { - return dp.userStorage.Put(ctx, record) -} -func (dp *DatabaseProvider) GetUserDataContainerByID(ctx context.Context, id *user.DataContainerID) (*storage.Record, error) { - return dp.userStorage.GetByID(ctx, id) -} -func (dp *DatabaseProvider) GetUserDataContainerByPhone(ctx context.Context, tokenAccount, phoneNumber string) (*storage.Record, error) { - identifyingFeatures := &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - } - return dp.userStorage.GetByFeatures(ctx, tokenAccount, identifyingFeatures) -} - // Timelock // -------------------------------------------------------------------------------- func (dp *DatabaseProvider) SaveTimelock(ctx context.Context, record *timelock.Record) error { @@ -1264,88 +801,6 @@ func (dp *DatabaseProvider) GetTimelockCountByState(ctx context.Context, state t return dp.timelock.GetCountByState(ctx, state) } -// Push -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) PutPushToken(ctx context.Context, record *push.Record) error { - return dp.push.Put(ctx, record) -} -func (dp *DatabaseProvider) MarkPushTokenAsInvalid(ctx context.Context, pushToken string) error { - return dp.push.MarkAsInvalid(ctx, pushToken) -} -func (dp *DatabaseProvider) DeletePushToken(ctx context.Context, pushToken string) error { - return dp.push.Delete(ctx, pushToken) -} -func (dp *DatabaseProvider) GetAllValidPushTokensdByDataContainer(ctx context.Context, id *user.DataContainerID) ([]*push.Record, error) { - return dp.push.GetAllValidByDataContainer(ctx, id) -} - -// Commitment -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) SaveCommitment(ctx context.Context, record *commitment.Record) error { - return dp.commitment.Save(ctx, record) -} -func (dp *DatabaseProvider) GetCommitmentByAddress(ctx context.Context, address string) (*commitment.Record, error) { - return dp.commitment.GetByAddress(ctx, address) -} -func (dp *DatabaseProvider) GetCommitmentByVault(ctx context.Context, vault string) (*commitment.Record, error) { - return dp.commitment.GetByVault(ctx, vault) -} -func (dp *DatabaseProvider) GetCommitmentByAction(ctx context.Context, intentId string, actionId uint32) (*commitment.Record, error) { - return dp.commitment.GetByAction(ctx, intentId, actionId) -} -func (dp *DatabaseProvider) GetAllCommitmentsByState(ctx context.Context, state commitment.State, opts ...query.Option) ([]*commitment.Record, error) { - req, err := query.DefaultPaginationHandler(opts...) - if err != nil { - return nil, err - } - - return dp.commitment.GetAllByState(ctx, state, req.Cursor, req.Limit, req.SortBy) -} -func (dp *DatabaseProvider) GetUpgradeableCommitmentsByOwner(ctx context.Context, owner string, limit uint64) ([]*commitment.Record, error) { - return dp.commitment.GetUpgradeableByOwner(ctx, owner, limit) -} -func (dp *DatabaseProvider) GetUsedTreasuryPoolDeficitFromCommitments(ctx context.Context, treasuryPool string) (uint64, error) { - return dp.commitment.GetUsedTreasuryPoolDeficit(ctx, treasuryPool) -} -func (dp *DatabaseProvider) GetTotalTreasuryPoolDeficitFromCommitments(ctx context.Context, treasuryPool string) (uint64, error) { - return dp.commitment.GetTotalTreasuryPoolDeficit(ctx, treasuryPool) -} -func (dp *DatabaseProvider) CountCommitmentsByState(ctx context.Context, state commitment.State) (uint64, error) { - return dp.commitment.CountByState(ctx, state) -} -func (dp *DatabaseProvider) CountPendingCommitmentRepaymentsDivertedToCommitment(ctx context.Context, address string) (uint64, error) { - return dp.commitment.CountPendingRepaymentsDivertedToCommitment(ctx, address) -} - -// Treasury Pool -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) SaveTreasuryPool(ctx context.Context, record *treasury.Record) error { - return dp.treasury.Save(ctx, record) -} -func (dp *DatabaseProvider) GetTreasuryPoolByName(ctx context.Context, name string) (*treasury.Record, error) { - return dp.treasury.GetByName(ctx, name) -} -func (dp *DatabaseProvider) GetTreasuryPoolByAddress(ctx context.Context, address string) (*treasury.Record, error) { - return dp.treasury.GetByAddress(ctx, address) -} -func (dp *DatabaseProvider) GetTreasuryPoolByVault(ctx context.Context, vault string) (*treasury.Record, error) { - return dp.treasury.GetByVault(ctx, vault) -} -func (dp *DatabaseProvider) GetAllTreasuryPoolsByState(ctx context.Context, state treasury.TreasuryPoolState, opts ...query.Option) ([]*treasury.Record, error) { - req, err := query.DefaultPaginationHandler(opts...) - if err != nil { - return nil, err - } - - return dp.treasury.GetAllByState(ctx, state, req.Cursor, req.Limit, req.SortBy) -} -func (dp *DatabaseProvider) SaveTreasuryPoolFunding(ctx context.Context, record *treasury.FundingHistoryRecord) error { - return dp.treasury.SaveFunding(ctx, record) -} -func (dp *DatabaseProvider) GetTotalAvailableTreasuryPoolFunds(ctx context.Context, vault string) (uint64, error) { - return dp.treasury.GetTotalAvailableFunds(ctx, vault) -} - // Merkle Tree func (dp *DatabaseProvider) InitializeNewMerkleTree(ctx context.Context, name string, levels uint8, seeds []merkletree.Seed, readOnly bool) (*merkletree.MerkleTree, error) { return merkletree.InitializeNew(ctx, dp.merkleTree, name, levels, seeds, readOnly) @@ -1393,24 +848,6 @@ func (dp *DatabaseProvider) GetRequest(ctx context.Context, intentId string) (*p return dp.paymentRequest.Get(ctx, intentId) } -// Paywall -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) CreatePaywall(ctx context.Context, record *paywall.Record) error { - return dp.paywall.Put(ctx, record) -} -func (dp *DatabaseProvider) GetPaywallByShortPath(ctx context.Context, path string) (*paywall.Record, error) { - return dp.paywall.GetByShortPath(ctx, path) -} - -// Event -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) SaveEvent(ctx context.Context, record *event.Record) error { - return dp.event.Save(ctx, record) -} -func (dp *DatabaseProvider) GetEvent(ctx context.Context, id string) (*event.Record, error) { - return dp.event.Get(ctx, id) -} - // Webhook // -------------------------------------------------------------------------------- func (dp *DatabaseProvider) CreateWebhook(ctx context.Context, record *webhook.Record) error { @@ -1429,74 +866,6 @@ func (dp *DatabaseProvider) GetAllPendingWebhooksReadyToSend(ctx context.Context return dp.webhook.GetAllPendingReadyToSend(ctx, limit) } -// Chat -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) PutChat(ctx context.Context, record *chat.Chat) error { - return dp.chat.PutChat(ctx, record) -} -func (dp *DatabaseProvider) GetChatById(ctx context.Context, chatId chat.ChatId) (*chat.Chat, error) { - return dp.chat.GetChatById(ctx, chatId) -} -func (dp *DatabaseProvider) GetAllChatsForUser(ctx context.Context, user string, opts ...query.Option) ([]*chat.Chat, error) { - req, err := query.DefaultPaginationHandler(opts...) - if err != nil { - return nil, err - } - return dp.chat.GetAllChatsForUser(ctx, user, req.Cursor, req.SortBy, req.Limit) -} -func (dp *DatabaseProvider) PutChatMessage(ctx context.Context, record *chat.Message) error { - return dp.chat.PutMessage(ctx, record) -} -func (dp *DatabaseProvider) DeleteChatMessage(ctx context.Context, chatId chat.ChatId, messageId string) error { - return dp.chat.DeleteMessage(ctx, chatId, messageId) -} -func (dp *DatabaseProvider) GetChatMessage(ctx context.Context, chatId chat.ChatId, messageId string) (*chat.Message, error) { - return dp.chat.GetMessageById(ctx, chatId, messageId) -} -func (dp *DatabaseProvider) GetAllChatMessages(ctx context.Context, chatId chat.ChatId, opts ...query.Option) ([]*chat.Message, error) { - req, err := query.DefaultPaginationHandler(opts...) - if err != nil { - return nil, err - } - return dp.chat.GetAllMessagesByChat(ctx, chatId, req.Cursor, req.SortBy, req.Limit) -} -func (dp *DatabaseProvider) AdvanceChatPointer(ctx context.Context, chatId chat.ChatId, pointer string) error { - return dp.chat.AdvancePointer(ctx, chatId, pointer) -} -func (dp *DatabaseProvider) GetChatUnreadCount(ctx context.Context, chatId chat.ChatId) (uint32, error) { - return dp.chat.GetUnreadCount(ctx, chatId) -} -func (dp *DatabaseProvider) SetChatMuteState(ctx context.Context, chatId chat.ChatId, isMuted bool) error { - return dp.chat.SetMuteState(ctx, chatId, isMuted) -} -func (dp *DatabaseProvider) SetChatSubscriptionState(ctx context.Context, chatId chat.ChatId, isSubscribed bool) error { - return dp.chat.SetSubscriptionState(ctx, chatId, isSubscribed) -} - -// Badge Count -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) AddToBadgeCount(ctx context.Context, owner string, amount uint32) error { - return dp.badgecount.Add(ctx, owner, amount) -} -func (dp *DatabaseProvider) ResetBadgeCount(ctx context.Context, owner string) error { - return dp.badgecount.Reset(ctx, owner) -} -func (dp *DatabaseProvider) GetBadgeCount(ctx context.Context, owner string) (*badgecount.Record, error) { - return dp.badgecount.Get(ctx, owner) -} - -// Login -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) SaveLogins(ctx context.Context, record *login.MultiRecord) error { - return dp.login.Save(ctx, record) -} -func (dp *DatabaseProvider) GetLoginsByAppInstall(ctx context.Context, appInstallId string) (*login.MultiRecord, error) { - return dp.login.GetAllByInstallId(ctx, appInstallId) -} -func (dp *DatabaseProvider) GetLatestLoginByOwner(ctx context.Context, owner string) (*login.Record, error) { - return dp.login.GetLatestByOwner(ctx, owner) -} - // Balance // -------------------------------------------------------------------------------- func (dp *DatabaseProvider) SaveBalanceCheckpoint(ctx context.Context, record *balance.Record) error { @@ -1515,69 +884,6 @@ func (dp *DatabaseProvider) GetFiatOnrampPurchase(ctx context.Context, nonce uui return dp.onramp.Get(ctx, nonce) } -// User Preferences -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) SaveUserPreferences(ctx context.Context, record *preferences.Record) error { - return dp.preferences.Save(ctx, record) -} -func (dp *DatabaseProvider) GetUserPreferences(ctx context.Context, id *user.DataContainerID) (*preferences.Record, error) { - return dp.preferences.Get(ctx, id) -} -func (dp *DatabaseProvider) GetUserLocale(ctx context.Context, owner string) (language.Tag, error) { - verificationRecord, err := dp.GetLatestPhoneVerificationForAccount(ctx, owner) - if err != nil { - return language.Und, errors.Wrap(err, "error getting latest phone verification record") - } - - dataContainerRecord, err := dp.GetUserDataContainerByPhone(ctx, owner, verificationRecord.PhoneNumber) - if err != nil { - return language.Und, errors.Wrap(err, "error getting data container record") - } - - userPreferencesRecord, err := dp.GetUserPreferences(ctx, dataContainerRecord.ID) - switch err { - case nil: - return userPreferencesRecord.Locale, nil - case preferences.ErrPreferencesNotFound: - return preferences.GetDefaultLocale(), nil - default: - return language.Und, errors.Wrap(err, "error getting user preferences record") - } -} - -// Airdrop -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) MarkIneligibleForAirdrop(ctx context.Context, owner string) error { - return dp.airdrop.MarkIneligible(ctx, owner) -} -func (dp *DatabaseProvider) IsEligibleForAirdrop(ctx context.Context, owner string) (bool, error) { - return dp.airdrop.IsEligible(ctx, owner) -} - -// Twitter -// -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) SaveTwitterUser(ctx context.Context, record *twitter.Record) error { - return dp.twitter.SaveUser(ctx, record) -} -func (dp *DatabaseProvider) GetTwitterUserByUsername(ctx context.Context, username string) (*twitter.Record, error) { - return dp.twitter.GetUserByUsername(ctx, username) -} -func (dp *DatabaseProvider) GetTwitterUserByTipAddress(ctx context.Context, tipAddress string) (*twitter.Record, error) { - return dp.twitter.GetUserByTipAddress(ctx, tipAddress) -} -func (dp *DatabaseProvider) GetStaleTwitterUsers(ctx context.Context, minAge time.Duration, limit int) ([]*twitter.Record, error) { - return dp.twitter.GetStaleUsers(ctx, minAge, limit) -} -func (dp *DatabaseProvider) MarkTweetAsProcessed(ctx context.Context, tweetId string) error { - return dp.twitter.MarkTweetAsProcessed(ctx, tweetId) -} -func (dp *DatabaseProvider) IsTweetProcessed(ctx context.Context, tweetId string) (bool, error) { - return dp.twitter.IsTweetProcessed(ctx, tweetId) -} -func (dp *DatabaseProvider) MarkTwitterNonceAsUsed(ctx context.Context, tweetId string, nonce uuid.UUID) error { - return dp.twitter.MarkNonceAsUsed(ctx, tweetId, nonce) -} - // VM RAM // -------------------------------------------------------------------------------- func (dp *DatabaseProvider) InitializeVmMemory(ctx context.Context, record *cvm_ram.Record) error { diff --git a/pkg/code/data/invite/v2/memory/store.go b/pkg/code/data/invite/v2/memory/store.go deleted file mode 100644 index 0bbed49b..00000000 --- a/pkg/code/data/invite/v2/memory/store.go +++ /dev/null @@ -1,191 +0,0 @@ -package memory - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/phone" - "github.com/code-payments/code-server/pkg/code/data/invite/v2" -) - -type store struct { - mu sync.RWMutex - usersByPhoneNumber map[string]*invite.User - influencerCodeByCode map[string]*invite.InfluencerCode -} - -// New returns a new in memory invite.v2.Store -func New() invite.Store { - return &store{ - usersByPhoneNumber: make(map[string]*invite.User), - influencerCodeByCode: make(map[string]*invite.InfluencerCode), - } -} - -// GetUser implements invite.v2.Store.GetUser -func (s *store) GetUser(_ context.Context, phoneNumber string) (*invite.User, error) { - s.mu.Lock() - defer s.mu.Unlock() - - user, ok := s.usersByPhoneNumber[phoneNumber] - if !ok { - return nil, invite.ErrUserNotFound - } - return user, nil -} - -// PutUser implements invite.v2.Store.PutUser -func (s *store) PutUser(ctx context.Context, user *invite.User) error { - if err := user.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - copy := &invite.User{ - PhoneNumber: user.PhoneNumber, - InvitedBy: user.InvitedBy, - Invited: user.Invited, - InviteCount: user.InviteCount, - InvitesSent: user.InvitesSent, - DepositInvitesReceived: user.DepositInvitesReceived, - IsRevoked: user.IsRevoked, - } - - _, alreadyExists := s.usersByPhoneNumber[copy.PhoneNumber] - if alreadyExists { - return invite.ErrAlreadyExists - } - - if copy.InvitedBy != nil { - - if phone.IsE164Format(*copy.InvitedBy) { - sender, ok := s.usersByPhoneNumber[*copy.InvitedBy] - if !ok || sender.InvitesSent >= sender.InviteCount || sender.IsRevoked { - return invite.ErrInviteCountExceeded - } - - sender.InvitesSent++ - } else { - influencer, ok := s.influencerCodeByCode[*copy.InvitedBy] - if !ok || influencer.InvitesSent >= influencer.InviteCount || influencer.IsRevoked || influencer.ExpiresAt.Before(time.Now()) { - return invite.ErrInviteCountExceeded - } - fmt.Printf("%+v\n", influencer) - influencer.InvitesSent++ - } - } - - s.usersByPhoneNumber[copy.PhoneNumber] = copy - - return nil -} - -func (s *store) GetInfluencerCode(ctx context.Context, code string) (*invite.InfluencerCode, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - var influencerCode *invite.InfluencerCode - var ok bool - - if influencerCode, ok = s.influencerCodeByCode[code]; !ok { - return nil, invite.ErrInfluencerCodeNotFound - } - - return influencerCode, nil -} - -func (s *store) PutInfluencerCode(ctx context.Context, influencerCode *invite.InfluencerCode) error { - if err := influencerCode.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - _, alreadyExists := s.influencerCodeByCode[influencerCode.Code] - if alreadyExists { - return invite.ErrInfluencerCodeAlreadyExists - } - - s.influencerCodeByCode[influencerCode.Code] = influencerCode - - return nil -} - -func (s *store) ClaimInfluencerCode(ctx context.Context, code string) error { - existingCode, err := s.GetInfluencerCode(ctx, code) - if err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - // Check if this code has been revoked - if existingCode.IsRevoked { - return invite.ErrInfluencerCodeRevoked - } - - if existingCode.InvitesSent >= existingCode.InviteCount { - return invite.ErrInviteCountExceeded - } - - if (!existingCode.ExpiresAt.IsZero()) && (existingCode.ExpiresAt.Before(time.Now())) { - return invite.ErrInfluencerCodeExpired - } - - // Update the influencer code - s.influencerCodeByCode[code].InvitesSent++ - - return nil -} - -// FilterInvitedNumbers implements invite.v2.Store.FilterInvitedNumbers -func (s *store) FilterInvitedNumbers(ctx context.Context, phoneNumbers []string) ([]*invite.User, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - var filtered []*invite.User - for _, phoneNumber := range phoneNumbers { - user, ok := s.usersByPhoneNumber[phoneNumber] - if ok { - filtered = append(filtered, user) - } - } - - return filtered, nil - -} - -// PutOnWaitlist implements invite.v2.Store.PutOnWaitlist -func (s *store) PutOnWaitlist(ctx context.Context, entry *invite.WaitlistEntry) error { - // There's no getter methods, so we don't actually save anything - return nil -} - -// GiveInvitesForDeposit implements invite.v2.Store.GiveInvitesForDeposit -func (s *store) GiveInvitesForDeposit(ctx context.Context, phoneNumber string, amount int) error { - s.mu.Lock() - defer s.mu.Unlock() - - data, ok := s.usersByPhoneNumber[phoneNumber] - if !ok { - return nil - } - - if data.DepositInvitesReceived { - return nil - } - - data.InviteCount += uint32(amount) - data.DepositInvitesReceived = true - return nil -} - -func (s *store) reset() { - s.usersByPhoneNumber = make(map[string]*invite.User) -} diff --git a/pkg/code/data/invite/v2/memory/store_test.go b/pkg/code/data/invite/v2/memory/store_test.go deleted file mode 100644 index c9099924..00000000 --- a/pkg/code/data/invite/v2/memory/store_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/invite/v2/tests" -) - -func TestInviteV2MemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/invite/v2/postgres/model.go b/pkg/code/data/invite/v2/postgres/model.go deleted file mode 100644 index 05428403..00000000 --- a/pkg/code/data/invite/v2/postgres/model.go +++ /dev/null @@ -1,290 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - "time" - - "github.com/jmoiron/sqlx" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - "github.com/code-payments/code-server/pkg/phone" - - "github.com/code-payments/code-server/pkg/code/data/invite/v2" -) - -const ( - userTableName = "codewallet__core_inviteuserv2" - influencerCodeTableName = "codewallet__core_influencercode" - waitlistTableName = "codewallet__core_invitewaitlist" -) - -type userModel struct { - Id sql.NullInt64 `db:"id"` - PhoneNumber string `db:"phone_number"` - InvitedBy sql.NullString `db:"invited_by"` - Invited time.Time `db:"invited"` - InviteCount int32 `db:"invite_count"` - InvitesSent int32 `db:"invites_sent"` - DepositInvitesReceived bool `db:"deposit_invites_received"` - IsRevoked bool `db:"is_revoked"` -} - -type influencerCodeModel struct { - Code string `db:"code"` - InviteCount int32 `db:"invite_count"` - InvitesSent int32 `db:"invites_sent"` - IsRevoked bool `db:"is_revoked"` - ExpiresAt time.Time `db:"expires_at"` -} - -type waitlistEntryModel struct { - Id sql.NullInt64 `db:"id"` - PhoneNumber string `db:"phone_number"` - IsVerified bool `db:"is_verified"` - CreatedAt time.Time `db:"created_at"` -} - -func toUserModel(obj *invite.User) (*userModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - invitedByValue := sql.NullString{} - if obj.InvitedBy != nil { - invitedByValue.Valid = true - invitedByValue.String = *obj.InvitedBy - } - - return &userModel{ - PhoneNumber: obj.PhoneNumber, - InvitedBy: invitedByValue, - Invited: obj.Invited, - InviteCount: int32(obj.InviteCount), - InvitesSent: int32(obj.InvitesSent), - DepositInvitesReceived: obj.DepositInvitesReceived, - IsRevoked: obj.IsRevoked, - }, nil -} - -func fromUserModel(obj *userModel) *invite.User { - var invitedByValue *string - if obj.InvitedBy.Valid { - invitedByValue = &obj.InvitedBy.String - } - - return &invite.User{ - PhoneNumber: obj.PhoneNumber, - InvitedBy: invitedByValue, - Invited: obj.Invited, - InviteCount: uint32(obj.InviteCount), - InvitesSent: uint32(obj.InvitesSent), - DepositInvitesReceived: obj.DepositInvitesReceived, - IsRevoked: obj.IsRevoked, - } -} - -func (m *userModel) dbPut(ctx context.Context, db *sqlx.DB) error { - tx, err := db.BeginTx(ctx, &sql.TxOptions{ - Isolation: sql.LevelDefault, - }) - if err != nil { - return err - } - - // Check if the InvitedBy value is a phone number by - - if m.InvitedBy.Valid { - var updateQuery string - - if phone.IsE164Format(m.InvitedBy.String) { - updateQuery = `UPDATE ` + userTableName + ` - SET invites_sent = invites_sent + 1 - WHERE phone_number = $1 AND is_revoked = false AND invites_sent < invite_count - ` - } else { - updateQuery = `UPDATE ` + influencerCodeTableName + ` - SET invites_sent = invites_sent + 1 - WHERE code = $1 AND is_revoked = false AND invites_sent < invite_count AND expires_at > NOW() - ` - } - - result, err := tx.ExecContext(ctx, updateQuery, m.InvitedBy.String) - if err != nil { - tx.Rollback() - return err - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - tx.Rollback() - return err - } else if rowsAffected == 0 { - tx.Rollback() - return invite.ErrInviteCountExceeded - } - } - - insertQuery := `INSERT INTO ` + userTableName + ` - (phone_number, invited_by, invited, invite_count, invites_sent, deposit_invites_received, is_revoked) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ` - _, err = tx.ExecContext( - ctx, - insertQuery, - m.PhoneNumber, - m.InvitedBy, - m.Invited.UTC(), - m.InviteCount, - m.InvitesSent, - m.DepositInvitesReceived, - m.IsRevoked, - ) - if err != nil { - tx.Rollback() - return pgutil.CheckUniqueViolation(err, invite.ErrAlreadyExists) - } - - return tx.Commit() -} - -func toWaitlistEntryModel(obj *invite.WaitlistEntry) *waitlistEntryModel { - return &waitlistEntryModel{ - PhoneNumber: obj.PhoneNumber, - IsVerified: obj.IsVerified, - CreatedAt: obj.CreatedAt, - } -} - -func (m *waitlistEntryModel) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + waitlistTableName + ` - (phone_number, is_verified, created_at) - VALUES ($1, $2, $3) - RETURNING id, phone_number, is_verified, created_at - ` - - err := db.QueryRowxContext( - ctx, - query, - m.PhoneNumber, - m.IsVerified, - m.CreatedAt, - ).StructScan(m) - return pgutil.CheckUniqueViolation(err, nil) -} - -func dbGetByNumber(ctx context.Context, db *sqlx.DB, phoneNumber string) (*userModel, error) { - res := &userModel{} - - query := `SELECT id, phone_number, invited_by, invited, invite_count, invites_sent, deposit_invites_received, is_revoked FROM ` + userTableName + ` - WHERE phone_number = $1` - - err := db.GetContext(ctx, res, query, phoneNumber) - if err != nil { - return nil, pgutil.CheckNoRows(err, invite.ErrUserNotFound) - } - return res, nil -} - -func dbFilterInvitedNumbers(ctx context.Context, db *sqlx.DB, phoneNumbers []string) ([]*userModel, error) { - if len(phoneNumbers) == 0 { - return nil, nil - } - - phoneNumberArgs := make([]string, len(phoneNumbers)) - for i, phoneNumber := range phoneNumbers { - phoneNumberArgs[i] = fmt.Sprintf("'%s'", phoneNumber) - } - - // todo: is there a better way to construct the query? - query := fmt.Sprintf( - "SELECT DISTINCT id, phone_number, invited_by, invited, invite_count, invites_sent, is_revoked FROM %s WHERE phone_number IN (%s)", - userTableName, - strings.Join(phoneNumberArgs, ","), - ) - - var res []*userModel - - err := db.SelectContext(ctx, &res, query) - return res, pgutil.CheckNoRows(err, nil) -} - -func dbGiveInvitesForDeposit(ctx context.Context, db *sqlx.DB, phoneNumber string, amount int) error { - if amount <= 0 { - return errors.New("invalid invite count delta") - } - - query := `UPDATE ` + userTableName + ` - SET invite_count = invite_count + $2, deposit_invites_received = true - WHERE phone_number = $1 AND deposit_invites_received IS FALSE AND is_revoked = false` - - _, err := db.ExecContext(ctx, query, phoneNumber, amount) - return pgutil.CheckNoRows(err, nil) -} - -func toInfluencerCodeModel(obj *invite.InfluencerCode) (*influencerCodeModel, error) { - return &influencerCodeModel{ - Code: obj.Code, - InviteCount: int32(obj.InviteCount), - InvitesSent: int32(obj.InvitesSent), - IsRevoked: obj.IsRevoked, - ExpiresAt: obj.ExpiresAt, - }, nil -} - -func fromInfluencerCodeModel(obj *influencerCodeModel) *invite.InfluencerCode { - return &invite.InfluencerCode{ - Code: obj.Code, - InviteCount: uint32(obj.InviteCount), - InvitesSent: uint32(obj.InvitesSent), - IsRevoked: obj.IsRevoked, - ExpiresAt: obj.ExpiresAt, - } -} - -func (m *influencerCodeModel) dbPut(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + influencerCodeTableName + ` - (code, invite_count, invites_sent, is_revoked, expires_at) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (code) DO UPDATE SET invite_count = $2, invites_sent = $3, is_revoked = $4, expires_at = $5 - ` - - _, err := db.ExecContext( - ctx, - query, - m.Code, - m.InviteCount, - m.InvitesSent, - m.IsRevoked, - m.ExpiresAt, - ) - return pgutil.CheckUniqueViolation(err, nil) -} - -func (m *influencerCodeModel) dbClaim(ctx context.Context, db *sqlx.DB) error { - query := `UPDATE ` + influencerCodeTableName + ` - SET invites_sent = invites_sent + 1 - WHERE code = $1 AND is_revoked = false AND invites_sent < invite_count AND expires_at > NOW() - RETURNING code, invite_count, invites_sent, is_revoked, expires_at` - - err := db.QueryRowxContext(ctx, query, m.Code).StructScan(m) - - return pgutil.CheckNoRows(err, invite.ErrInfluencerCodeNotFound) -} - -func dbGetInfluencerCode(ctx context.Context, db *sqlx.DB, code string) (*influencerCodeModel, error) { - res := &influencerCodeModel{} - - query := `SELECT code, invite_count, invites_sent, is_revoked, expires_at FROM ` + influencerCodeTableName + ` - WHERE code = $1` - - err := db.GetContext(ctx, res, query, code) - if err != nil { - return nil, pgutil.CheckNoRows(err, invite.ErrInfluencerCodeNotFound) - } - - return res, nil -} diff --git a/pkg/code/data/invite/v2/postgres/model_test.go b/pkg/code/data/invite/v2/postgres/model_test.go deleted file mode 100644 index 0a1d21f2..00000000 --- a/pkg/code/data/invite/v2/postgres/model_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package postgres - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/invite/v2" -) - -func TestUserModelConversion(t *testing.T) { - adminUser := &invite.User{ - PhoneNumber: "+11234567890", - InvitedBy: nil, - Invited: time.Now(), - InviteCount: 5, - InvitesSent: 2, - IsRevoked: false, - } - - model, err := toUserModel(adminUser) - require.NoError(t, err) - - actual := fromUserModel(model) - assert.EqualValues(t, adminUser, actual) - - invitedUser := &invite.User{ - PhoneNumber: "+11234567890", - InvitedBy: &adminUser.PhoneNumber, - Invited: time.Now(), - InviteCount: 5, - InvitesSent: 2, - IsRevoked: true, - } - - model, err = toUserModel(invitedUser) - require.NoError(t, err) - - actual = fromUserModel(model) - assert.EqualValues(t, invitedUser, actual) -} - -func TestInfluencerCodeModelConversion(t *testing.T) { - code := &invite.InfluencerCode{ - Code: "test-code", - InviteCount: 5, - InvitesSent: 2, - IsRevoked: false, - ExpiresAt: time.Now(), - } - - model, err := toInfluencerCodeModel(code) - require.NoError(t, err) - - actual := fromInfluencerCodeModel(model) - assert.EqualValues(t, code, actual) -} diff --git a/pkg/code/data/invite/v2/postgres/store.go b/pkg/code/data/invite/v2/postgres/store.go deleted file mode 100644 index 4a3417d4..00000000 --- a/pkg/code/data/invite/v2/postgres/store.go +++ /dev/null @@ -1,109 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/invite/v2" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres backed invite.v2.Store -func New(db *sql.DB) invite.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// GetUser implements invite.v2.Store.Getuser -func (s *store) GetUser(ctx context.Context, phoneNumber string) (*invite.User, error) { - model, err := dbGetByNumber(ctx, s.db, phoneNumber) - if err != nil { - return nil, err - } - - return fromUserModel(model), nil -} - -// PutUser implements invite.v2.Store.PutUser -func (s *store) PutUser(ctx context.Context, user *invite.User) error { - model, err := toUserModel(user) - if err != nil { - return err - } - - return model.dbPut(ctx, s.db) -} - -// GetInfluencerCode gets an influencer code. -func (s *store) GetInfluencerCode(ctx context.Context, code string) (*invite.InfluencerCode, error) { - model, err := dbGetInfluencerCode(ctx, s.db, code) - if err != nil { - return nil, err - } - - return fromInfluencerCodeModel(model), nil -} - -// PutInfluencerCode stores an influencer code. -func (s *store) PutInfluencerCode(ctx context.Context, influencerCode *invite.InfluencerCode) error { - model, err := toInfluencerCodeModel(influencerCode) - if err != nil { - return err - } - - return model.dbPut(ctx, s.db) -} - -// ClaimInfluencerCode claims an influencer code. -func (s *store) ClaimInfluencerCode(ctx context.Context, code string) error { - model, err := dbGetInfluencerCode(ctx, s.db, code) - if err != nil { - return err - } - - if model.IsRevoked { - return invite.ErrInfluencerCodeRevoked - } - - if model.InvitesSent >= model.InviteCount { - return invite.ErrInviteCountExceeded - } - - if (!model.ExpiresAt.IsZero()) && (model.ExpiresAt.Before(time.Now())) { - return invite.ErrInfluencerCodeExpired - } - - return model.dbClaim(ctx, s.db) -} - -// FilterInvitedNumbers implements invite.v2.Store.FilterInvitedNumbers -func (s *store) FilterInvitedNumbers(ctx context.Context, phoneNumbers []string) ([]*invite.User, error) { - models, err := dbFilterInvitedNumbers(ctx, s.db, phoneNumbers) - if err != nil { - return nil, err - } - - users := make([]*invite.User, len(models)) - for i, model := range models { - users[i] = fromUserModel(model) - } - return users, nil -} - -// PutOnWaitlist implements invite.v2.Store.PutOnWaitlist -func (s *store) PutOnWaitlist(ctx context.Context, entry *invite.WaitlistEntry) error { - model := toWaitlistEntryModel(entry) - return model.dbSave(ctx, s.db) -} - -// GiveInvitesForDeposit implements invite.v2.Store.GiveInvitesForDeposit -func (s *store) GiveInvitesForDeposit(ctx context.Context, phoneNumber string, amount int) error { - return dbGiveInvitesForDeposit(ctx, s.db, phoneNumber, amount) -} diff --git a/pkg/code/data/invite/v2/postgres/store_test.go b/pkg/code/data/invite/v2/postgres/store_test.go deleted file mode 100644 index 0462c6ed..00000000 --- a/pkg/code/data/invite/v2/postgres/store_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/invite/v2" - "github.com/code-payments/code-server/pkg/code/data/invite/v2/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -var ( - testStore invite.Store - teardown func() -) - -type Schema struct { - create string - drop string -} - -var defaultSchema = Schema{ - // Used for testing ONLY, the table and migrations are external to this repository - create: ` - CREATE TABLE codewallet__core_inviteuserv2( - id SERIAL NOT NULL PRIMARY KEY, - - phone_number TEXT NOT NULL, - invited_by TEXT, - invited TIMESTAMP WITH TIME ZONE, - invite_count INTEGER NOT NULL, - invites_sent INTEGER NOT NULL, - deposit_invites_received BOOL NOT NULL, - is_revoked BOOL NOT NULL, - - CONSTRAINT codewallet__core_inviteuserv2__uniq__phone_number UNIQUE (phone_number) - ); - - CREATE TABLE codewallet__core_influencercode( - code TEXT NOT NULL PRIMARY KEY, - invite_count INTEGER NOT NULL, - invites_sent INTEGER NOT NULL, - is_revoked BOOL NOT NULL, - expires_at TIMESTAMP WITH TIME ZONE - ); - `, - // Used for testing ONLY, the table and migrations are external to this repository - drop: ` - DROP TABLE codewallet__core_inviteuserv2; - DROP TABLE codewallet__core_influencercode; - `, -} - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestInviteV2PostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(defaultSchema.create) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(defaultSchema.drop) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/invite/v2/store.go b/pkg/code/data/invite/v2/store.go deleted file mode 100644 index 9641a6c9..00000000 --- a/pkg/code/data/invite/v2/store.go +++ /dev/null @@ -1,131 +0,0 @@ -package invite - -import ( - "context" - "errors" - "time" - - "github.com/code-payments/code-server/pkg/phone" -) - -var ( - // ErrUserNotFound is returned when a user is not found, which is equivalent - // to saying the user has not been invited - ErrUserNotFound = errors.New("user not invited") - - // ErrInviteCountExceeded is returned when a new user is attempted to be - // added to the database via an invite from someone who has reached - // their max count or has never been invited. - ErrInviteCountExceeded = errors.New("invite count exceeded") - - // ErrAlreadyExists is returned when a user already exists in the database. - ErrAlreadyExists = errors.New("user already invited") - - // ErrInfluencerCodeAlreadyExists is returned when an influencer code already exists. - ErrInfluencerCodeAlreadyExists = errors.New("influencer code already exists") - - // ErrInfluencerCodeNotFound is returned when an influencer code is not found. - ErrInfluencerCodeNotFound = errors.New("influencer code not found") - - // ErrInfluencerCodeRevoked is returned when an influencer code is revoked. - ErrInfluencerCodeRevoked = errors.New("influencer code revoked") - - // ErrInfluencerCodeExpired is returned when an influencer code is expired. - ErrInfluencerCodeExpired = errors.New("influencer code expired") -) - -// This view of a user is intentionally separated from the pkg/code/data/user -// model. Invites are going to be a temporary phase of the app, and this makes -// a clear separation of what can be removed. As it's defined right now, invites -// are also grouped by phone number, whereas a user is a 1:1 mapping between an -// ID and (phone number, token account) pair. -type User struct { - PhoneNumber string - InvitedBy *string - Invited time.Time - - InviteCount uint32 - InvitesSent uint32 - - DepositInvitesReceived bool - - IsRevoked bool -} - -type InfluencerCode struct { - Code string - - InviteCount uint32 - InvitesSent uint32 - - IsRevoked bool - ExpiresAt time.Time -} - -type WaitlistEntry struct { - PhoneNumber string - IsVerified bool - CreatedAt time.Time -} - -type Store interface { - // GetUser gets an invited user. - GetUser(ctx context.Context, phoneNumber string) (*User, error) - - // PutUser stores an invited user. If the inviter is provided, the function - // will verify that it is allowed to invite the user and will decrement its - // invite count on success. - PutUser(ctx context.Context, user *User) error - - // GetInfluencerCode gets an influencer code. - GetInfluencerCode(ctx context.Context, code string) (*InfluencerCode, error) - - // PutInfluencerCode stores an influencer code. - PutInfluencerCode(ctx context.Context, influencerCode *InfluencerCode) error - - // ClaimInfluencerCode claims an influencer code. - ClaimInfluencerCode(ctx context.Context, code string) error - - // FilterInvitedNumbers filters numbers that have been invited from the - // provided list and returns the associated set of users. - FilterInvitedNumbers(ctx context.Context, phoneNumbers []string) ([]*User, error) - - // PutOnWaitlist puts a user on a waitlist for invitation - PutOnWaitlist(ctx context.Context, entry *WaitlistEntry) error - - // GiveInvitesForDeposit one-time gives invites for depositing Kin into Code - GiveInvitesForDeposit(ctx context.Context, phoneNumber string, amount int) error -} - -// Validate validates an invited user model. -func (u *User) Validate() error { - if u == nil { - return errors.New("invite user is nil") - } - - if !phone.IsE164Format(u.PhoneNumber) { - return errors.New("invitee phone number doesn't match E.164 standard") - } - - if u.Invited.IsZero() { - return errors.New("invitation time is zero") - } - - if u.InviteCount < u.InvitesSent { - return errors.New("invites sent cannot exceed invite count") - } - - return nil -} - -func (i *InfluencerCode) Validate() error { - if i == nil { - return errors.New("influencer code is nil") - } - - if i.Code == "" { - return errors.New("influencer code is empty") - } - - return nil -} diff --git a/pkg/code/data/invite/v2/tests/tests.go b/pkg/code/data/invite/v2/tests/tests.go deleted file mode 100644 index cc52241a..00000000 --- a/pkg/code/data/invite/v2/tests/tests.go +++ /dev/null @@ -1,526 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/invite/v2" -) - -func RunTests(t *testing.T, s invite.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s invite.Store){ - testHappyPath, - testInviteSameUserTwice, - testExceedsInviteCount, - testInvitedByNonInvitedUser, - testFilterInvitedNumbers, - testRevokedInvite, - testGiveInvitesForDeposit, - - testInfluencerCodeClaim, - testInfluencerCodeExpired, - testInfluencerCodeRevoked, - - testInfluencerCodePutUser, - testInfluencerCodeRevokedPutUser, - testInfluencerCodeExpiredPutUser, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s invite.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - adminUser := &invite.User{ - PhoneNumber: "+11234567890", - Invited: time.Now(), - InviteCount: 5, - } - require.NoError(t, s.PutUser(ctx, adminUser)) - - actual, err := s.GetUser(ctx, adminUser.PhoneNumber) - require.NoError(t, err) - assertEqualUserEntries(t, adminUser, actual) - - invitedUser := &invite.User{ - PhoneNumber: "+12223334444", - InvitedBy: &adminUser.PhoneNumber, - InviteCount: 3, - Invited: time.Now(), - } - - _, err = s.GetUser(ctx, invitedUser.PhoneNumber) - assert.Equal(t, invite.ErrUserNotFound, err) - - require.NoError(t, s.PutUser(ctx, invitedUser)) - - actual, err = s.GetUser(ctx, adminUser.PhoneNumber) - require.NoError(t, err) - assert.EqualValues(t, 5, actual.InviteCount) - assert.EqualValues(t, 1, actual.InvitesSent) - - actual, err = s.GetUser(ctx, invitedUser.PhoneNumber) - require.NoError(t, err) - assertEqualUserEntries(t, invitedUser, actual) - }) -} - -func testInviteSameUserTwice(t *testing.T, s invite.Store) { - t.Run("testInviteSameUserTwice", func(t *testing.T) { - ctx := context.Background() - - adminUser := &invite.User{ - PhoneNumber: "+11234567890", - Invited: time.Now(), - InviteCount: 5, - } - require.NoError(t, s.PutUser(ctx, adminUser)) - assert.Equal(t, invite.ErrAlreadyExists, s.PutUser(ctx, adminUser)) - - invitedUser := &invite.User{ - PhoneNumber: "+12223334444", - InvitedBy: &adminUser.PhoneNumber, - Invited: time.Now(), - } - - require.NoError(t, s.PutUser(ctx, invitedUser)) - assert.Equal(t, invite.ErrAlreadyExists, s.PutUser(ctx, invitedUser)) - - actual, err := s.GetUser(ctx, adminUser.PhoneNumber) - require.NoError(t, err) - assert.EqualValues(t, 1, actual.InvitesSent) - }) -} - -func testExceedsInviteCount(t *testing.T, s invite.Store) { - t.Run("testExceedsInviteCount", func(t *testing.T) { - ctx := context.Background() - - adminUser := &invite.User{ - PhoneNumber: "+11234567890", - Invited: time.Now(), - InviteCount: 5, - } - require.NoError(t, s.PutUser(ctx, adminUser)) - - for i := 0; i < 5; i++ { - invitedUser := &invite.User{ - PhoneNumber: fmt.Sprintf("+1800555000%d", i), - InvitedBy: &adminUser.PhoneNumber, - Invited: time.Now(), - } - require.NoError(t, s.PutUser(ctx, invitedUser)) - } - - actual, err := s.GetUser(ctx, adminUser.PhoneNumber) - require.NoError(t, err) - require.EqualValues(t, 5, actual.InviteCount) - require.EqualValues(t, 5, actual.InvitesSent) - - invitedUser := &invite.User{ - PhoneNumber: "+12223334444", - InvitedBy: &adminUser.PhoneNumber, - Invited: time.Now(), - } - assert.Equal(t, invite.ErrInviteCountExceeded, s.PutUser(ctx, invitedUser)) - - _, err = s.GetUser(ctx, invitedUser.PhoneNumber) - assert.Equal(t, invite.ErrUserNotFound, err) - - actual, err = s.GetUser(ctx, adminUser.PhoneNumber) - require.NoError(t, err) - assert.EqualValues(t, 5, actual.InviteCount) - assert.EqualValues(t, 5, actual.InvitesSent) - }) -} - -func testInvitedByNonInvitedUser(t *testing.T, s invite.Store) { - t.Run("testInvitedByNonInvitedUser", func(t *testing.T) { - ctx := context.Background() - - invitedBy := "+11234567890" - user := &invite.User{ - PhoneNumber: "+12223334444", - InvitedBy: &invitedBy, - Invited: time.Now(), - InviteCount: 5, - } - assert.Equal(t, invite.ErrInviteCountExceeded, s.PutUser(ctx, user)) - }) -} - -func testFilterInvitedNumbers(t *testing.T, s invite.Store) { - t.Run("testFilterInvitedNumbers", func(t *testing.T) { - ctx := context.Background() - - adminUser := &invite.User{ - PhoneNumber: "+11234567890", - Invited: time.Now(), - InviteCount: 100, - } - require.NoError(t, s.PutUser(ctx, adminUser)) - - filtered, err := s.FilterInvitedNumbers(ctx, []string{}) - require.NoError(t, err) - assert.Empty(t, filtered) - - var phoneNumbers []string - for i := 0; i < 10; i++ { - phoneNumbers = append(phoneNumbers, fmt.Sprintf("+1800555000%d", i)) - } - - filtered, err = s.FilterInvitedNumbers(ctx, phoneNumbers) - require.NoError(t, err) - assert.Empty(t, filtered) - - var invitedUsers []*invite.User - for _, phoneNumber := range phoneNumbers[:len(phoneNumbers)/2] { - invitedUser := &invite.User{ - PhoneNumber: phoneNumber, - InvitedBy: &adminUser.PhoneNumber, - Invited: time.Now(), - } - require.NoError(t, s.PutUser(ctx, invitedUser)) - - invitedUsers = append(invitedUsers, invitedUser) - } - - filtered, err = s.FilterInvitedNumbers(ctx, phoneNumbers) - require.NoError(t, err) - - require.Len(t, filtered, len(phoneNumbers)/2) - for i, actual := range filtered { - assertEqualUserEntries(t, invitedUsers[i], actual) - } - }) -} - -func testRevokedInvite(t *testing.T, s invite.Store) { - t.Run("testRevokedInvite", func(t *testing.T) { - ctx := context.Background() - - adminUser := &invite.User{ - PhoneNumber: "+11234567890", - Invited: time.Now(), - InviteCount: 100, - IsRevoked: true, - } - require.NoError(t, s.PutUser(ctx, adminUser)) - - actual, err := s.GetUser(ctx, adminUser.PhoneNumber) - require.NoError(t, err) - assertEqualUserEntries(t, adminUser, actual) - - invitedUser := &invite.User{ - PhoneNumber: "+12223334444", - InvitedBy: &adminUser.PhoneNumber, - Invited: time.Now(), - } - err = s.PutUser(ctx, invitedUser) - assert.Equal(t, invite.ErrInviteCountExceeded, err) - - _, err = s.GetUser(ctx, invitedUser.PhoneNumber) - assert.Equal(t, invite.ErrUserNotFound, err) - - actual, err = s.GetUser(ctx, adminUser.PhoneNumber) - require.NoError(t, err) - assertEqualUserEntries(t, adminUser, actual) - }) -} - -func testGiveInvitesForDeposit(t *testing.T, s invite.Store) { - t.Run("testGiveInvitesForDeposit", func(t *testing.T) { - ctx := context.Background() - - for i, alreadyReceived := range []bool{true, false} { - inviteUser := &invite.User{ - PhoneNumber: fmt.Sprintf("+1800555000%d", i), - Invited: time.Now(), - InviteCount: 100, - DepositInvitesReceived: alreadyReceived, - } - require.NoError(t, s.PutUser(ctx, inviteUser)) - - actual, err := s.GetUser(ctx, inviteUser.PhoneNumber) - require.NoError(t, err) - assertEqualUserEntries(t, inviteUser, actual) - - for i := 0; i < 5; i++ { - require.NoError(t, s.GiveInvitesForDeposit(ctx, inviteUser.PhoneNumber, 5)) - - actual, err = s.GetUser(ctx, inviteUser.PhoneNumber) - require.NoError(t, err) - assert.True(t, actual.DepositInvitesReceived) - if alreadyReceived { - assert.EqualValues(t, 100, actual.InviteCount) - } else { - assert.EqualValues(t, 105, actual.InviteCount) - } - } - } - }) -} - -func assertEqualUserEntries(t *testing.T, obj1, obj2 *invite.User) { - require.NoError(t, obj1.Validate()) - require.NoError(t, obj2.Validate()) - - assert.Equal(t, obj1.PhoneNumber, obj2.PhoneNumber) - assert.Equal(t, obj1.Invited.Unix(), obj2.Invited.Unix()) - assert.Equal(t, obj1.InviteCount, obj2.InviteCount) - assert.Equal(t, obj1.InvitesSent, obj2.InvitesSent) - assert.Equal(t, obj1.DepositInvitesReceived, obj2.DepositInvitesReceived) - assert.Equal(t, obj1.IsRevoked, obj2.IsRevoked) - - if obj1.InvitedBy == nil { - assert.Nil(t, obj2.InvitedBy) - } else { - require.NotNil(t, obj1.InvitedBy) - assert.Equal(t, obj1.InvitedBy, obj2.InvitedBy) - } -} - -func testInfluencerCodeClaim(t *testing.T, s invite.Store) { - t.Run("testInfluencerCodeClaim", func(t *testing.T) { - ctx := context.Background() - - inviteCode := "mrbeast" - - // Create a influencer code - influencerCode := &invite.InfluencerCode{ - Code: inviteCode, - InviteCount: 10, - InvitesSent: 2, - IsRevoked: false, - ExpiresAt: time.Now().Add(time.Hour), - } - - // Add the influencer code to the store - require.NoError(t, s.PutInfluencerCode(ctx, influencerCode)) - - // Get the influencer code from the store - actual, err := s.GetInfluencerCode(ctx, influencerCode.Code) - require.NoError(t, err) - - // Assert the influencer code is the same - assert.EqualValues(t, influencerCode.Code, actual.Code) - assert.EqualValues(t, influencerCode.InviteCount, actual.InviteCount) - assert.EqualValues(t, influencerCode.InvitesSent, actual.InvitesSent) - assert.EqualValues(t, influencerCode.IsRevoked, actual.IsRevoked) - assert.EqualValues(t, influencerCode.ExpiresAt.Unix(), actual.ExpiresAt.Unix()) - - // Test multiple claims - for i := uint32(influencerCode.InvitesSent + 1); i <= influencerCode.InviteCount; i++ { - - // Claim the influencer code - err = s.ClaimInfluencerCode(ctx, inviteCode) - assert.NoError(t, err) - - // Get the influencer code from the store - actual, err = s.GetInfluencerCode(ctx, influencerCode.Code) - - // Assert that no error occurred - assert.NoError(t, err) - - // Assert that the influencer code was claimed - assert.EqualValues(t, influencerCode.Code, actual.Code) - assert.EqualValues(t, influencerCode.InviteCount, actual.InviteCount) - assert.EqualValues(t, i, actual.InvitesSent) - assert.EqualValues(t, influencerCode.IsRevoked, actual.IsRevoked) - assert.EqualValues(t, influencerCode.ExpiresAt.Unix(), actual.ExpiresAt.Unix()) - } - - // Claim the influencer code - err = s.ClaimInfluencerCode(ctx, inviteCode) - - // Assert that the code has been used up - assert.Equal(t, invite.ErrInviteCountExceeded, err) - }) -} - -func testInfluencerCodeRevoked(t *testing.T, s invite.Store) { - t.Run("testInfluencerCodeRevoked", func(t *testing.T) { - ctx := context.Background() - - // Create a influencer code - influencerCode := &invite.InfluencerCode{ - Code: "cristiano", - InviteCount: 100, - InvitesSent: 42, - IsRevoked: true, - ExpiresAt: time.Now().Add(time.Hour), - } - - // Add the influencer code to the store - require.NoError(t, s.PutInfluencerCode(ctx, influencerCode)) - - // Claim the influencer code - err := s.ClaimInfluencerCode(ctx, "cristiano") - - // Assert that the code has been revoked - assert.Equal(t, invite.ErrInfluencerCodeRevoked, err) - }) -} - -func testInfluencerCodeExpired(t *testing.T, s invite.Store) { - t.Run("testInfluencerCodeExpired", func(t *testing.T) { - ctx := context.Background() - - // Create a influencer code - influencerCode := &invite.InfluencerCode{ - Code: "leomessi", - InviteCount: 100, - InvitesSent: 42, - IsRevoked: false, - ExpiresAt: time.Now().Add(-time.Hour), - } - - // Add the influencer code to the store - require.NoError(t, s.PutInfluencerCode(ctx, influencerCode)) - - // Claim the influencer code - err := s.ClaimInfluencerCode(ctx, "leomessi") - - // Assert that the code has expired - assert.Equal(t, invite.ErrInfluencerCodeExpired, err) - }) -} - -func testInfluencerCodePutUser(t *testing.T, s invite.Store) { - t.Run("testInfluencerCodePutUser", func(t *testing.T) { - ctx := context.Background() - - invitedBy := "anatoly" - - // Create a influencer code - influencerCode := &invite.InfluencerCode{ - Code: invitedBy, - InviteCount: 10, - InvitesSent: 2, - IsRevoked: false, - ExpiresAt: time.Now().Add(time.Hour), - } - - // Add the influencer code to the store - require.NoError(t, s.PutInfluencerCode(ctx, influencerCode)) - - // Get the influencer code from the store - actual, err := s.GetInfluencerCode(ctx, influencerCode.Code) - require.NoError(t, err) - - // Assert the influencer code is the same - assert.EqualValues(t, influencerCode.Code, actual.Code) - assert.EqualValues(t, influencerCode.InviteCount, actual.InviteCount) - assert.EqualValues(t, influencerCode.InvitesSent, actual.InvitesSent) - assert.EqualValues(t, influencerCode.IsRevoked, actual.IsRevoked) - assert.EqualValues(t, influencerCode.ExpiresAt.Unix(), actual.ExpiresAt.Unix()) - - // Test multiple claims - for i := uint32(influencerCode.InvitesSent + 1); i <= influencerCode.InviteCount; i++ { - - // Put a user to claim the code - phoneNumber := fmt.Sprintf("+1800555000%d", i) - invitedUser := &invite.User{ - PhoneNumber: phoneNumber, - InvitedBy: &invitedBy, - Invited: time.Now(), - } - require.NoError(t, s.PutUser(ctx, invitedUser)) - - // Get the influencer code from the store - actual, err = s.GetInfluencerCode(ctx, influencerCode.Code) - - // Assert that no error occurred - assert.NoError(t, err) - - // Assert that the influencer code was claimed - assert.EqualValues(t, influencerCode.Code, actual.Code) - assert.EqualValues(t, influencerCode.InviteCount, actual.InviteCount) - assert.EqualValues(t, i, actual.InvitesSent) - assert.EqualValues(t, influencerCode.IsRevoked, actual.IsRevoked) - assert.EqualValues(t, influencerCode.ExpiresAt.Unix(), actual.ExpiresAt.Unix()) - } - - phoneNumber := "+18005559001" - invitedUser := &invite.User{ - PhoneNumber: phoneNumber, - InvitedBy: &invitedBy, - Invited: time.Now(), - } - - // Assert that the code has been used up - assert.Equal(t, invite.ErrInviteCountExceeded, s.PutUser(ctx, invitedUser)) - }) -} - -func testInfluencerCodeRevokedPutUser(t *testing.T, s invite.Store) { - t.Run("testInfluencerCodeRevokedPutUser", func(t *testing.T) { - ctx := context.Background() - - inviteCode := "vitalik" - - // Create a influencer code - influencerCode := &invite.InfluencerCode{ - Code: inviteCode, - InviteCount: 100, - InvitesSent: 42, - IsRevoked: true, - ExpiresAt: time.Now().Add(time.Hour), - } - - // Add the influencer code to the store - require.NoError(t, s.PutInfluencerCode(ctx, influencerCode)) - - phoneNumber := "+18005559001" - invitedUser := &invite.User{ - PhoneNumber: phoneNumber, - InvitedBy: &inviteCode, - Invited: time.Now(), - } - - // TODO: this should be ErrInviteCodeRevoked but following what the old code does... - - // Assert that the code has been revoked - assert.Equal(t, invite.ErrInviteCountExceeded, s.PutUser(ctx, invitedUser)) - }) -} - -func testInfluencerCodeExpiredPutUser(t *testing.T, s invite.Store) { - t.Run("testInfluencerCodeExpiredPutUser", func(t *testing.T) { - ctx := context.Background() - - inviteCode := "lamport" - - // Create a influencer code - influencerCode := &invite.InfluencerCode{ - Code: inviteCode, - InviteCount: 100, - InvitesSent: 42, - IsRevoked: false, - ExpiresAt: time.Now().Add(-time.Hour), - } - - // Add the influencer code to the store - require.NoError(t, s.PutInfluencerCode(ctx, influencerCode)) - - phoneNumber := "+18005559001" - invitedUser := &invite.User{ - PhoneNumber: phoneNumber, - InvitedBy: &inviteCode, - Invited: time.Now(), - } - - // TODO: this should be ErrInviteCodeExpired but following what the old code does... - - // Assert that the code has been revoked - assert.Equal(t, invite.ErrInviteCountExceeded, s.PutUser(ctx, invitedUser)) - }) -} diff --git a/pkg/code/data/login/login.go b/pkg/code/data/login/login.go deleted file mode 100644 index d18f7c88..00000000 --- a/pkg/code/data/login/login.go +++ /dev/null @@ -1,65 +0,0 @@ -package login - -import ( - "errors" - "time" -) - -type MultiRecord struct { - AppInstallId string - Owners []string - LastUpdatedAt time.Time -} - -type Record struct { - AppInstallId string - Owner string - LastUpdatedAt time.Time -} - -func (r *MultiRecord) Validate() error { - if len(r.AppInstallId) == 0 { - return errors.New("app install id is required") - } - - // todo: If we upgrade this, ensure to update tests to verify implementations - if len(r.Owners) > 1 { - return errors.New("at most one owner can be associated to an app install") - } - - for _, owner := range r.Owners { - if len(owner) == 0 { - return errors.New("owner is required when set") - } - } - - return nil -} - -func (r *MultiRecord) Clone() MultiRecord { - return MultiRecord{ - AppInstallId: r.AppInstallId, - Owners: append([]string(nil), r.Owners...), - - LastUpdatedAt: r.LastUpdatedAt, - } -} - -func (r *MultiRecord) CopyTo(dst *MultiRecord) { - dst.AppInstallId = r.AppInstallId - dst.Owners = append([]string(nil), r.Owners...) - - dst.LastUpdatedAt = r.LastUpdatedAt -} - -func (r *Record) Validate() error { - if len(r.AppInstallId) == 0 { - return errors.New("app install id is required") - } - - if len(r.Owner) > 1 { - return errors.New("owner is required") - } - - return nil -} diff --git a/pkg/code/data/login/memory/store.go b/pkg/code/data/login/memory/store.go deleted file mode 100644 index 4403568e..00000000 --- a/pkg/code/data/login/memory/store.go +++ /dev/null @@ -1,121 +0,0 @@ -package memory - -import ( - "context" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/code/data/login" -) - -type store struct { - mu sync.Mutex - records []*login.MultiRecord -} - -// New returns a new in memory login.Store -func New() login.Store { - return &store{} -} - -// Save implements login.Store.Save -func (s *store) Save(_ context.Context, data *login.MultiRecord) error { - s.mu.Lock() - defer s.mu.Unlock() - - var found bool - for _, item := range s.records { - if item.AppInstallId == data.AppInstallId { - item.Owners = append([]string(nil), data.Owners...) - item.LastUpdatedAt = time.Now() - item.CopyTo(data) - found = true - continue - } - - var owners []string - for _, owner := range item.Owners { - var excludeOwner bool - for _, updatedOwner := range data.Owners { - if owner == updatedOwner { - excludeOwner = true - break - } - } - if !excludeOwner { - owners = append(owners, owner) - } - } - item.Owners = owners - item.LastUpdatedAt = time.Now() - } - - if !found { - data.LastUpdatedAt = time.Now() - cloned := data.Clone() - s.records = append(s.records, &cloned) - } - - return nil -} - -// GetAllByInstallId implements login.Store.GetAllByInstallId -func (s *store) GetAllByInstallId(_ context.Context, appInstallId string) (*login.MultiRecord, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if item := s.findByAppInstallId(appInstallId); item != nil { - if len(item.Owners) == 0 { - return nil, login.ErrLoginNotFound - } - - cloned := item.Clone() - return &cloned, nil - } - - return nil, login.ErrLoginNotFound -} - -// GetLatestByOwner implements login.Store.GetLatestByOwner -func (s *store) GetLatestByOwner(_ context.Context, owner string) (*login.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findByOwner(owner) - if item == nil { - return nil, login.ErrLoginNotFound - } - - return &login.Record{ - AppInstallId: item.AppInstallId, - Owner: owner, - LastUpdatedAt: item.LastUpdatedAt, - }, nil -} - -func (s *store) findByAppInstallId(appInstallId string) *login.MultiRecord { - for _, item := range s.records { - if item.AppInstallId == appInstallId { - return item - } - } - return nil -} - -func (s *store) findByOwner(owner string) *login.MultiRecord { - for _, item := range s.records { - for _, itemOwner := range item.Owners { - if itemOwner == owner { - return item - } - } - } - return nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.records = nil -} diff --git a/pkg/code/data/login/memory/store_test.go b/pkg/code/data/login/memory/store_test.go deleted file mode 100644 index 20bfa5bf..00000000 --- a/pkg/code/data/login/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/login/tests" -) - -func TestLoginMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/login/postgres/model.go b/pkg/code/data/login/postgres/model.go deleted file mode 100644 index d1396ca0..00000000 --- a/pkg/code/data/login/postgres/model.go +++ /dev/null @@ -1,152 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "errors" - "time" - - "github.com/jmoiron/sqlx" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - "github.com/code-payments/code-server/pkg/code/data/login" -) - -const ( - tableName = "codewallet__core_applogin" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - - AppInstallId string `db:"app_install_id"` - Owner string `db:"owner"` - - LastUpdatedAt time.Time `db:"last_updated_at"` -} - -func toModels(item *login.MultiRecord) ([]*model, error) { - if err := item.Validate(); err != nil { - return nil, err - } - - var res []*model - for _, owner := range item.Owners { - res = append(res, &model{ - AppInstallId: item.AppInstallId, - Owner: owner, - LastUpdatedAt: item.LastUpdatedAt, - }) - } - return res, nil -} - -func fromModel(m *model) *login.Record { - return &login.Record{ - AppInstallId: m.AppInstallId, - Owner: m.Owner, - LastUpdatedAt: m.LastUpdatedAt, - } -} - -func fromModels(appInstallId string, models []*model) (*login.MultiRecord, error) { - res := &login.MultiRecord{ - AppInstallId: appInstallId, - LastUpdatedAt: time.Now(), - } - - var lastUpdatedAt time.Time - for _, model := range models { - if model.AppInstallId != appInstallId { - return nil, errors.New("models are not from the expected app install") - } - - res.Owners = append(res.Owners, model.Owner) - - if model.LastUpdatedAt.After(lastUpdatedAt) { - lastUpdatedAt = model.LastUpdatedAt - } - } - - if len(models) > 0 { - res.LastUpdatedAt = lastUpdatedAt - } - - return res, nil -} - -func (m *model) dbSaveInTx(ctx context.Context, tx *sqlx.Tx) error { - m.LastUpdatedAt = time.Now() - - err := dbDeleteAllByInstallIdInTx(ctx, tx, m.AppInstallId) - if err != nil { - return err - } - - err = dbDeleteAllByOwnerInTx(ctx, tx, m.Owner) - if err != nil { - return err - } - - query := `INSERT INTO ` + tableName + ` - (app_install_id, owner, last_updated_at) - VALUES ($1, $2, $3) - RETURNING id, app_install_id, owner, last_updated_at` - _, err = tx.ExecContext( - ctx, - query, - m.AppInstallId, - m.Owner, - m.LastUpdatedAt, - ) - return err -} - -func dbDeleteAllByInstallIdInTx(ctx context.Context, tx *sqlx.Tx, appInstallId string) error { - query := `DELETE FROM ` + tableName + ` - WHERE app_install_id = $1` - _, err := tx.ExecContext( - ctx, - query, - appInstallId, - ) - return err -} - -func dbDeleteAllByOwnerInTx(ctx context.Context, tx *sqlx.Tx, owner string) error { - query := `DELETE FROM ` + tableName + ` - WHERE owner = $1` - _, err := tx.ExecContext( - ctx, - query, - owner, - ) - return err -} - -func dbGetAllByInstallId(ctx context.Context, db *sqlx.DB, appInstallId string) ([]*model, error) { - var res []*model - - query := `SELECT id, app_install_id, owner, last_updated_at FROM ` + tableName + ` - WHERE app_install_id = $1` - err := db.SelectContext(ctx, &res, query, appInstallId) - if err != nil { - return nil, pgutil.CheckNoRows(err, login.ErrLoginNotFound) - } - if len(res) == 0 { - return nil, login.ErrLoginNotFound - } - return res, nil -} - -func dbGetLatestByOwner(ctx context.Context, db *sqlx.DB, owner string) (*model, error) { - var res model - - query := `SELECT id, app_install_id, owner, last_updated_at FROM ` + tableName + ` - WHERE owner = $1` - err := db.GetContext(ctx, &res, query, owner) - if err != nil { - return nil, pgutil.CheckNoRows(err, login.ErrLoginNotFound) - } - return &res, nil -} diff --git a/pkg/code/data/login/postgres/store.go b/pkg/code/data/login/postgres/store.go deleted file mode 100644 index 3d24de00..00000000 --- a/pkg/code/data/login/postgres/store.go +++ /dev/null @@ -1,82 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/login" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new in psotres login.Store -func New(db *sql.DB) login.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Save implements login.Store.Save -// -// todo: An awkward implementation, but enables a quick migration to multi-device -// logins if that every becomes a thing. -func (s *store) Save(ctx context.Context, record *login.MultiRecord) error { - models, err := toModels(record) - if err != nil { - return err - } - - var newRecord *login.MultiRecord - err = pgutil.ExecuteInTx(ctx, s.db, sql.LevelDefault, func(tx *sqlx.Tx) error { - for _, model := range models { - err := model.dbSaveInTx(ctx, tx) - if err != nil { - return err - } - } - - if len(models) == 0 { - err = dbDeleteAllByInstallIdInTx(ctx, tx, record.AppInstallId) - if err != nil { - return err - } - } - - newRecord, err = fromModels(record.AppInstallId, models) - if err != nil { - return err - } - - return nil - }) - if err != nil { - return err - } - - newRecord.CopyTo(record) - return nil -} - -// GetAllByInstallId implements login.Store.GetAllByInstallId -func (s *store) GetAllByInstallId(ctx context.Context, appInstallId string) (*login.MultiRecord, error) { - models, err := dbGetAllByInstallId(ctx, s.db, appInstallId) - if err != nil { - return nil, err - } - - return fromModels(appInstallId, models) -} - -// GetLatestByOwner implements login.Store.GetLatestByOwner -func (s *store) GetLatestByOwner(ctx context.Context, owner string) (*login.Record, error) { - model, err := dbGetLatestByOwner(ctx, s.db, owner) - if err != nil { - return nil, err - } - return fromModel(model), nil -} diff --git a/pkg/code/data/login/postgres/store_test.go b/pkg/code/data/login/postgres/store_test.go deleted file mode 100644 index d05bbada..00000000 --- a/pkg/code/data/login/postgres/store_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/login" - "github.com/code-payments/code-server/pkg/code/data/login/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -var ( - testStore login.Store - teardown func() -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_applogin ( - id SERIAL NOT NULL PRIMARY KEY, - - app_install_id TEXT NOT NULL, - owner TEXT NOT NULL, - - last_updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_applogin__uniq__app_install_id UNIQUE (app_install_id), - CONSTRAINT codewallet__core_applogin__uniq__owner UNIQUE (owner) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_applogin; - ` -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestLoginPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/login/store.go b/pkg/code/data/login/store.go deleted file mode 100644 index 6d798d40..00000000 --- a/pkg/code/data/login/store.go +++ /dev/null @@ -1,23 +0,0 @@ -package login - -import ( - "context" - "errors" -) - -var ( - ErrLoginNotFound = errors.New("login not found") -) - -type Store interface { - // Save saves a multi login record in a single DB tx - Save(ctx context.Context, record *MultiRecord) error - - // GetAllByInstallId gets a multi login record for an app install. If no - // logins are detected, ErrLoginNotFound is returned. - GetAllByInstallId(ctx context.Context, appInstallId string) (*MultiRecord, error) - - // GetLatestByOwner gets the latest login record for an owner. If no - // login is detected, ErrLoginNotFound is returned. - GetLatestByOwner(ctx context.Context, owner string) (*Record, error) -} diff --git a/pkg/code/data/login/tests/tests.go b/pkg/code/data/login/tests/tests.go deleted file mode 100644 index e3874dd4..00000000 --- a/pkg/code/data/login/tests/tests.go +++ /dev/null @@ -1,142 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/login" -) - -func RunTests(t *testing.T, s login.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s login.Store){ - testHappyPath, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s login.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - _, err := s.GetAllByInstallId(ctx, "app-install-1") - assert.Equal(t, login.ErrLoginNotFound, err) - - _, err = s.GetLatestByOwner(ctx, "owner1") - assert.Equal(t, login.ErrLoginNotFound, err) - - start := time.Now() - expected := &login.MultiRecord{ - AppInstallId: "app-install-1", - Owners: []string{"owner1"}, - } - require.NoError(t, s.Save(ctx, expected)) - assert.Equal(t, "app-install-1", expected.AppInstallId) - assert.Equal(t, "owner1", expected.Owners[0]) - assert.True(t, expected.LastUpdatedAt.After(start)) - - expected = &login.MultiRecord{ - AppInstallId: "app-install-2", - Owners: []string{"owner2"}, - } - require.NoError(t, s.Save(ctx, expected)) - assert.Equal(t, "app-install-2", expected.AppInstallId) - assert.Equal(t, "owner2", expected.Owners[0]) - assert.True(t, expected.LastUpdatedAt.After(start)) - - multiActual, err := s.GetAllByInstallId(ctx, "app-install-1") - require.NoError(t, err) - assert.Equal(t, "app-install-1", multiActual.AppInstallId) - require.Len(t, multiActual.Owners, 1) - assert.Equal(t, "owner1", multiActual.Owners[0]) - - multiActual, err = s.GetAllByInstallId(ctx, "app-install-2") - require.NoError(t, err) - assert.Equal(t, "app-install-2", multiActual.AppInstallId) - require.Len(t, multiActual.Owners, 1) - assert.Equal(t, "owner2", multiActual.Owners[0]) - - require.NoError(t, s.Save(ctx, &login.MultiRecord{ - AppInstallId: "app-install-3", - Owners: []string{"owner1"}, - })) - - _, err = s.GetAllByInstallId(ctx, "app-install-1") - assert.Equal(t, login.ErrLoginNotFound, err) - - multiActual, err = s.GetAllByInstallId(ctx, "app-install-2") - require.NoError(t, err) - assert.Equal(t, "app-install-2", multiActual.AppInstallId) - require.Len(t, multiActual.Owners, 1) - assert.Equal(t, "owner2", multiActual.Owners[0]) - - multiActual, err = s.GetAllByInstallId(ctx, "app-install-3") - require.NoError(t, err) - assert.Equal(t, "app-install-3", multiActual.AppInstallId) - require.Len(t, multiActual.Owners, 1) - assert.Equal(t, "owner1", multiActual.Owners[0]) - - start = time.Now() - expected = &login.MultiRecord{ - AppInstallId: "app-install-2", - Owners: []string{"owner3"}, - } - require.NoError(t, s.Save(ctx, expected)) - assert.Equal(t, "app-install-2", expected.AppInstallId) - assert.Equal(t, "owner3", expected.Owners[0]) - assert.True(t, expected.LastUpdatedAt.After(start)) - - _, err = s.GetAllByInstallId(ctx, "app-install-1") - assert.Equal(t, login.ErrLoginNotFound, err) - - multiActual, err = s.GetAllByInstallId(ctx, "app-install-2") - require.NoError(t, err) - assert.Equal(t, "app-install-2", multiActual.AppInstallId) - require.Len(t, multiActual.Owners, 1) - assert.Equal(t, "owner3", multiActual.Owners[0]) - - multiActual, err = s.GetAllByInstallId(ctx, "app-install-3") - require.NoError(t, err) - assert.Equal(t, "app-install-3", multiActual.AppInstallId) - require.Len(t, multiActual.Owners, 1) - assert.Equal(t, "owner1", multiActual.Owners[0]) - - start = time.Now() - expected = &login.MultiRecord{ - AppInstallId: "app-install-2", - Owners: nil, - } - require.NoError(t, s.Save(ctx, expected)) - assert.Equal(t, "app-install-2", expected.AppInstallId) - assert.Empty(t, expected.Owners) - assert.True(t, expected.LastUpdatedAt.After(start)) - - _, err = s.GetAllByInstallId(ctx, "app-install-1") - assert.Equal(t, login.ErrLoginNotFound, err) - - _, err = s.GetAllByInstallId(ctx, "app-install-2") - assert.Equal(t, login.ErrLoginNotFound, err) - - multiActual, err = s.GetAllByInstallId(ctx, "app-install-3") - require.NoError(t, err) - assert.Equal(t, "app-install-3", multiActual.AppInstallId) - require.Len(t, multiActual.Owners, 1) - assert.Equal(t, "owner1", multiActual.Owners[0]) - - singleActual, err := s.GetLatestByOwner(ctx, "owner1") - require.NoError(t, err) - assert.Equal(t, "app-install-3", singleActual.AppInstallId) - assert.Equal(t, "owner1", singleActual.Owner) - - _, err = s.GetLatestByOwner(ctx, "owner2") - assert.Equal(t, login.ErrLoginNotFound, err) - - _, err = s.GetLatestByOwner(ctx, "owner3") - assert.Equal(t, login.ErrLoginNotFound, err) - }) -} diff --git a/pkg/code/data/payment/memory/store.go b/pkg/code/data/payment/memory/store.go deleted file mode 100644 index e5ea0dad..00000000 --- a/pkg/code/data/payment/memory/store.go +++ /dev/null @@ -1,357 +0,0 @@ -package memory - -import ( - "context" - "fmt" - "sort" - "strings" - "sync" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/payment" - "github.com/code-payments/code-server/pkg/code/data/transaction" -) - -type store struct { - paymentRecordMu sync.Mutex - paymentRecords map[string]*payment.Record - lastIndex uint64 -} - -type ById []*payment.Record - -func (a ById) Len() int { return len(a) } -func (a ById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ById) Less(i, j int) bool { return a[i].Id < a[j].Id } - -func New() payment.Store { - return &store{ - paymentRecords: make(map[string]*payment.Record), - lastIndex: 1, - } -} - -func (s *store) reset() { - s.paymentRecordMu.Lock() - s.paymentRecords = make(map[string]*payment.Record) - s.lastIndex = 1 - s.paymentRecordMu.Unlock() -} - -func (s *store) getPk(signature string, index uint32) string { - return fmt.Sprint(signature, index) -} - -func (s *store) Put(ctx context.Context, data *payment.Record) error { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - pk := s.getPk(data.TransactionId, data.TransactionIndex) - - if _, ok := s.paymentRecords[pk]; ok { - return payment.ErrExists - } - - data.Id = s.lastIndex - data.ExchangeCurrency = strings.ToLower(data.ExchangeCurrency) - s.paymentRecords[pk] = data - s.lastIndex += 1 - return nil -} - -func (s *store) Get(ctx context.Context, txId string, index uint32) (*payment.Record, error) { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - for _, item := range s.paymentRecords { - if item.TransactionId == txId && item.TransactionIndex == index { - return item, nil - } - } - - return nil, payment.ErrNotFound -} - -func (s *store) Update(ctx context.Context, data *payment.Record) error { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - var result *payment.Record - for _, item := range s.paymentRecords { - if item.Id == data.Id { - result = item - } - } - - if result != nil { - result.ExchangeCurrency = strings.ToLower(data.ExchangeCurrency) - result.Region = data.Region - result.ExchangeRate = data.ExchangeRate - result.UsdMarketValue = data.UsdMarketValue - result.ConfirmationState = data.ConfirmationState - } else { - return payment.ErrNotFound - } - - return nil -} - -func (s *store) GetAllForTransaction(ctx context.Context, txId string) ([]*payment.Record, error) { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - all := make([]*payment.Record, 0) - for _, item := range s.paymentRecords { - if item.TransactionId == txId { - all = append(all, item) - } - } - - if len(all) == 0 { - return nil, payment.ErrNotFound - } - - return all, nil -} - -func (s *store) GetAllForAccount(ctx context.Context, account string, cursor uint64, limit uint, ordering query.Ordering) (result []*payment.Record, err error) { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - if limit == 0 { - return nil, payment.ErrNotFound - } - - // not ideal, but this is for testing purposes and s.paymentRecord should be small - all := make([]*payment.Record, 0) - for _, record := range s.paymentRecords { - if record.Source == account || record.Destination == account { - if ordering == query.Ascending { - if record.Id > cursor { - all = append(all, record) - } - } else { - if cursor == 0 || record.Id < cursor { - all = append(all, record) - } - } - } - } - - sort.Sort(ById(all)) - - if ordering == query.Descending { - for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 { - all[i], all[j] = all[j], all[i] - } - } - - if len(all) == 0 { - return nil, payment.ErrNotFound - } - - if len(all) < int(limit) { - return all, nil - } - - return all[:limit], nil -} - -func (s *store) GetAllForAccountByType(ctx context.Context, account string, cursor uint64, limit uint, ordering query.Ordering, paymentType payment.PaymentType) (result []*payment.Record, err error) { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - if limit == 0 { - return nil, payment.ErrNotFound - } - - // not ideal, but this is for testing purposes and s.paymentRecord should be small - all := make([]*payment.Record, 0) - for _, record := range s.paymentRecords { - if (paymentType == payment.PaymentType_Send && record.Source == account) || - (paymentType == payment.PaymentType_Receive && record.Destination == account) { - - if ordering == query.Ascending { - if record.Id > cursor { - all = append(all, record) - } - } else { - if record.Id < cursor { - all = append(all, record) - } - } - } - } - - sort.Sort(ById(all)) - - if ordering == query.Descending { - for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 { - all[i], all[j] = all[j], all[i] - } - } - - if len(all) == 0 { - return nil, payment.ErrNotFound - } - - if len(all) < int(limit) { - return all, nil - } - - return all[:limit], nil -} - -func (s *store) GetAllForAccountByTypeAfterBlock(ctx context.Context, account string, block uint64, cursor uint64, limit uint, ordering query.Ordering, paymentType payment.PaymentType) (result []*payment.Record, err error) { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - if limit == 0 { - return nil, payment.ErrNotFound - } - - // not ideal, but this is for testing purposes and s.paymentRecord should be small - all := make([]*payment.Record, 0) - for _, record := range s.paymentRecords { - if record.BlockId <= block { - continue - } - - if (paymentType == payment.PaymentType_Send && record.Source == account) || - (paymentType == payment.PaymentType_Receive && record.Destination == account) { - - if ordering == query.Ascending { - if record.Id > cursor { - all = append(all, record) - } - } else { - if record.Id < cursor { - all = append(all, record) - } - } - } - } - - sort.Sort(ById(all)) - - if ordering == query.Descending { - for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 { - all[i], all[j] = all[j], all[i] - } - } - - if len(all) == 0 { - return nil, payment.ErrNotFound - } - - if len(all) < int(limit) { - return all, nil - } - - return all[:limit], nil -} - -func (s *store) GetAllForAccountByTypeWithinBlockRange(ctx context.Context, account string, lowerBound, upperBound uint64, cursor uint64, limit uint, ordering query.Ordering, paymentType payment.PaymentType) ([]*payment.Record, error) { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - if limit == 0 { - return nil, payment.ErrNotFound - } - - // not ideal, but this is for testing purposes and s.paymentRecord should be small - all := make([]*payment.Record, 0) - for _, record := range s.paymentRecords { - if record.BlockId <= lowerBound || record.BlockId >= upperBound { - continue - } - - if (paymentType == payment.PaymentType_Send && record.Source == account) || - (paymentType == payment.PaymentType_Receive && record.Destination == account) { - - if ordering == query.Ascending { - if record.Id > cursor { - all = append(all, record) - } - } else { - if record.Id < cursor { - all = append(all, record) - } - } - } - } - - sort.Sort(ById(all)) - - if ordering == query.Descending { - for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 { - all[i], all[j] = all[j], all[i] - } - } - - if len(all) == 0 { - return nil, payment.ErrNotFound - } - - if len(all) < int(limit) { - return all, nil - } - - return all[:limit], nil -} - -func (s *store) GetAllExternalDepositsAfterBlock(ctx context.Context, account string, block uint64, cursor uint64, limit uint, ordering query.Ordering) ([]*payment.Record, error) { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - if limit == 0 { - return nil, payment.ErrNotFound - } - - // not ideal, but this is for testing purposes and s.paymentRecord should be small - all := make([]*payment.Record, 0) - for _, record := range s.paymentRecords { - if record.IsExternal && record.Destination == account && record.BlockId > block { - if ordering == query.Ascending { - if record.Id > cursor { - all = append(all, record) - } - } else { - if record.Id < cursor { - all = append(all, record) - } - } - } - } - - sort.Sort(ById(all)) - - if ordering == query.Descending { - for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 { - all[i], all[j] = all[j], all[i] - } - } - - if len(all) == 0 { - return nil, payment.ErrNotFound - } - - if len(all) < int(limit) { - return all, nil - } - - return all[:limit], nil -} - -func (s *store) GetExternalDepositAmount(ctx context.Context, account string) (uint64, error) { - s.paymentRecordMu.Lock() - defer s.paymentRecordMu.Unlock() - - var res uint64 - for _, record := range s.paymentRecords { - if record.IsExternal && record.Destination == account && record.ConfirmationState == transaction.ConfirmationFinalized { - res += record.Quantity - } - } - return res, nil -} diff --git a/pkg/code/data/payment/memory/store_test.go b/pkg/code/data/payment/memory/store_test.go deleted file mode 100644 index a0545775..00000000 --- a/pkg/code/data/payment/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/payment/tests" -) - -func TestPaymentMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/payment/payment.go b/pkg/code/data/payment/payment.go deleted file mode 100644 index 71d92c07..00000000 --- a/pkg/code/data/payment/payment.go +++ /dev/null @@ -1,105 +0,0 @@ -package payment - -import ( - "bytes" - "time" - - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/solana/token" - "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/mr-tron/base58" - "github.com/pkg/errors" -) - -type ByBlock []*Record - -func (a ByBlock) Len() int { return len(a) } -func (a ByBlock) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByBlock) Less(i, j int) bool { return a[i].BlockId < a[j].BlockId } - -// The structure for metadata behind a payment or token transfer between two -// parties. This data is considered untrusted as it comes from the client apps -// directly and not from the blockchain. It gives us the intended native -// currencies for the agreed upon exchange between two app users. This data -// cannot be derived from the blockchain alone. We could guess at it, but then -// we would definitely be off by a couple decimal points every now and then when -// reporting the booking cost back to the user. -// -// Note: This is generally unused right now and should be deprecated with the -// new intent system and external data models. There's a few use cases still -// hitting this which, in particular, need to know the order of transfers. -type Record struct { - Id uint64 // The internal database id for this transaction - - BlockId uint64 - BlockTime time.Time - TransactionId string // The signature of the Solana transaction, which could contain multiple payments - TransactionIndex uint32 // The index that the transfer (payment) instruction appears at inside the Solana transaction - Rendezvous string // The public key of the party that is the rendezvous point for this payment (might be empty) - IsExternal bool // External payments are deprecated, in favour of the new deposit store - - Source string // The source account id for this payment - Destination string // The destination account id for this payment - Quantity uint64 // The amount of Kin (in Quarks) - - ExchangeCurrency string // The (external) agreed upon currency for the exchange - ExchangeRate float64 // The (external) agreed upon exchange rate for determining the amount of Kin to transfer - UsdMarketValue float64 // The (internal) market value of this transfer based on the internal exchange rate record - Region *string // The (external) agreed upon country flag for the currency - - IsWithdraw bool - - ConfirmationState transaction.Confirmation - CreatedAt time.Time -} - -type PaymentType uint32 - -const ( - PaymentType_Send PaymentType = iota - PaymentType_Receive -) - -func NewFromTransfer(transfer *token.DecompiledTransfer, sig string, index int, rate float64, now time.Time) *Record { - source_id := base58.Encode(transfer.Source) - destination_id := base58.Encode(transfer.Destination) - - return &Record{ - TransactionId: sig, - TransactionIndex: uint32(index), - Source: source_id, - Destination: destination_id, - Quantity: transfer.Amount, - ExchangeCurrency: "kin", - ExchangeRate: 1, - UsdMarketValue: rate * float64(kin.FromQuarks(transfer.Amount)), - ConfirmationState: transaction.ConfirmationPending, - CreatedAt: now, - } -} - -func NewFromTransferChecked(transfer *token.DecompiledTransfer2, sig string, index int, rate float64, now time.Time) (*Record, error) { - if !bytes.Equal(transfer.Mint, kin.TokenMint) { - return nil, errors.New("invalid token mint") - } - - if transfer.Decimals != kin.Decimals { - return nil, errors.New("invalid kin token decimals") - } - - source_id := base58.Encode(transfer.Source) - destination_id := base58.Encode(transfer.Destination) - - return &Record{ - TransactionId: sig, - TransactionIndex: uint32(index), - Source: source_id, - Destination: destination_id, - Quantity: transfer.Amount, - ExchangeCurrency: "kin", - ExchangeRate: 1, - UsdMarketValue: rate * float64(kin.FromQuarks(transfer.Amount)), - ConfirmationState: transaction.ConfirmationPending, - CreatedAt: now, - }, nil -} diff --git a/pkg/code/data/payment/postgres/model.go b/pkg/code/data/payment/postgres/model.go deleted file mode 100644 index a9d5969a..00000000 --- a/pkg/code/data/payment/postgres/model.go +++ /dev/null @@ -1,362 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "strings" - "time" - - "github.com/jmoiron/sqlx" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - q "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/payment" - "github.com/code-payments/code-server/pkg/code/data/transaction" -) - -const ( - tableName = "codewallet__core_payment" - - tableColumns = ` - block_id, - block_time, - transaction_id, - transaction_index, - rendezvous_key, - is_external, - - source, - destination, - quantity, - - exchange_currency, - region, - exchange_rate, - usd_market_value, - - is_withdraw, - - confirmation_state, - created_at - ` -) - -type model struct { - Id sql.NullInt64 `db:"id"` - BlockId sql.NullInt64 `db:"block_id"` - BlockTime sql.NullTime `db:"block_time"` - TransactionId string `db:"transaction_id"` - TransactionIndex uint32 `db:"transaction_index"` - Rendezvous sql.NullString `db:"rendezvous_key"` - IsExternal bool `db:"is_external"` - SourceId string `db:"source"` - DestinationId string `db:"destination"` - Quantity uint64 `db:"quantity"` - ExchangeCurrency string `db:"exchange_currency"` - Region sql.NullString `db:"region"` - ExchangeRate float64 `db:"exchange_rate"` - UsdMarketValue float64 `db:"usd_market_value"` - IsWithdraw bool `db:"is_withdraw"` - ConfirmationState transaction.Confirmation `db:"confirmation_state"` - CreatedAt time.Time `db:"created_at"` -} - -func toModel(obj *payment.Record) *model { - m := &model{ - Id: sql.NullInt64{Int64: int64(obj.Id), Valid: obj.Id > 0}, - BlockId: sql.NullInt64{Int64: int64(obj.BlockId), Valid: obj.BlockId > 0}, - BlockTime: sql.NullTime{Time: obj.BlockTime, Valid: !obj.BlockTime.IsZero()}, - TransactionId: obj.TransactionId, - TransactionIndex: obj.TransactionIndex, - Rendezvous: sql.NullString{String: obj.Rendezvous, Valid: obj.Rendezvous != ""}, - IsExternal: obj.IsExternal, - SourceId: obj.Source, - DestinationId: obj.Destination, - Quantity: obj.Quantity, - ExchangeCurrency: strings.ToLower(obj.ExchangeCurrency), - ExchangeRate: obj.ExchangeRate, - UsdMarketValue: obj.UsdMarketValue, - IsWithdraw: obj.IsWithdraw, - ConfirmationState: obj.ConfirmationState, - CreatedAt: obj.CreatedAt.UTC(), - } - - if obj.Region != nil { - m.Region.Valid = true - m.Region.String = strings.ToLower(*obj.Region) - } - - return m -} - -func fromModel(obj *model) *payment.Record { - record := &payment.Record{ - Id: uint64(obj.Id.Int64), - BlockId: uint64(obj.BlockId.Int64), - BlockTime: obj.BlockTime.Time.UTC(), - TransactionId: obj.TransactionId, - TransactionIndex: obj.TransactionIndex, - Rendezvous: obj.Rendezvous.String, - IsExternal: obj.IsExternal, - Source: obj.SourceId, - Destination: obj.DestinationId, - Quantity: obj.Quantity, - ExchangeCurrency: strings.ToLower(obj.ExchangeCurrency), - ExchangeRate: obj.ExchangeRate, - UsdMarketValue: obj.UsdMarketValue, - IsWithdraw: obj.IsWithdraw, - ConfirmationState: obj.ConfirmationState, - CreatedAt: obj.CreatedAt.UTC(), - } - - if obj.Region.Valid { - record.Region = &obj.Region.String - } - - return record -} - -func (self *model) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + tableName + ` (` + tableColumns + `) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING *;` - - err := db.QueryRowxContext(ctx, query, - self.BlockId, - self.BlockTime, - self.TransactionId, - self.TransactionIndex, - self.Rendezvous, - self.IsExternal, - self.SourceId, - self.DestinationId, - self.Quantity, - self.ExchangeCurrency, - self.Region, - self.ExchangeRate, - self.UsdMarketValue, - self.IsWithdraw, - self.ConfirmationState, - self.CreatedAt, - ).StructScan(self) - - return pgutil.CheckUniqueViolation(err, payment.ErrExists) -} - -func (self *model) dbUpdate(ctx context.Context, db *sqlx.DB) error { - query := `UPDATE ` + tableName + ` - SET - block_id = $2, - block_time = $3, - exchange_currency = $4, - region = $5, - exchange_rate = $6, - usd_market_value = $7, - confirmation_state = $8 - WHERE id = $1 RETURNING *;` - - err := db.QueryRowxContext(ctx, query, - self.Id, - self.BlockId, - self.BlockTime, - self.ExchangeCurrency, - self.Region, - self.ExchangeRate, - self.UsdMarketValue, - self.ConfirmationState, - ).StructScan(self) - - return pgutil.CheckNoRows(err, payment.ErrNotFound) -} - -func makeSelectQuery(condition string, ordering q.Ordering) string { - return `SELECT * FROM ` + tableName + ` WHERE ` + condition + ` ORDER BY id ` + q.FromOrderingWithFallback(ordering, "asc") -} - -func makeGetQuery(condition string, ordering q.Ordering) string { - return makeSelectQuery(condition, ordering) + ` LIMIT 1` -} - -func makeGetAllQuery(condition string, ordering q.Ordering, withCursor bool) string { - var query string - - query = "SELECT * FROM " + tableName + " WHERE" - - if withCursor { - if ordering == q.Ascending { - query = query + " id > $3 AND" - } else { - query = query + " id < $3 AND" - } - } else { - // Nonsense condition to make sure we get all records - // TODO: This is a hack, we should use a proper way to get all records - query = query + " (id < $3 OR id >= $3) AND " - } - - query = query + " (" + condition + ") " - query = query + " ORDER BY id " + q.FromOrderingWithFallback(ordering, "ASC") - query = query + " LIMIT $2" - - return query -} - -func dbGet(ctx context.Context, db *sqlx.DB, txId string, index uint32) (*model, error) { - res := &model{} - err := db.GetContext(ctx, res, - makeGetQuery("transaction_id = $1 AND transaction_index = $2", q.Descending), - txId, - index, - ) - - return res, pgutil.CheckNoRows(err, payment.ErrNotFound) -} - -func dbGetAllForTransaction(ctx context.Context, db *sqlx.DB, txId string) ([]*model, error) { - res := []*model{} - err := db.SelectContext(ctx, &res, - makeSelectQuery("transaction_id = $1", q.Descending), - txId, - ) - - if err != nil { - return nil, pgutil.CheckNoRows(err, payment.ErrNotFound) - } - if len(res) == 0 { - return nil, payment.ErrNotFound - } - - return res, nil -} - -func dbGetAllForAccount(ctx context.Context, db *sqlx.DB, account string, cursor uint64, limit uint, ordering q.Ordering) ([]*model, error) { - res := []*model{} - err := db.SelectContext(ctx, &res, - makeGetAllQuery("source = $1 OR destination = $1", ordering, cursor > 0), - account, limit, cursor, - ) - - if err != nil { - return nil, pgutil.CheckNoRows(err, payment.ErrNotFound) - } - if len(res) == 0 { - return nil, payment.ErrNotFound - } - - return res, nil -} - -func dbGetAllForAccountByType(ctx context.Context, db *sqlx.DB, account string, cursor uint64, limit uint, ordering q.Ordering, paymentType payment.PaymentType) ([]*model, error) { - res := []*model{} - - var condition string - if paymentType == payment.PaymentType_Send { - condition = "source = $1" - } else { - condition = "destination = $1" - } - - err := db.SelectContext(ctx, &res, - makeGetAllQuery(condition, ordering, cursor > 0), - account, limit, cursor, - ) - - if err != nil { - return nil, pgutil.CheckNoRows(err, payment.ErrNotFound) - } - if len(res) == 0 { - return nil, payment.ErrNotFound - } - - return res, nil -} - -func dbGetAllForAccountByTypeAfterBlock(ctx context.Context, db *sqlx.DB, account string, block uint64, cursor uint64, limit uint, ordering q.Ordering, paymentType payment.PaymentType) ([]*model, error) { - res := []*model{} - - var condition string - if paymentType == payment.PaymentType_Send { - condition = "source = $1" - } else { - condition = "destination = $1" - } - - condition += " AND block_id > $4" - - err := db.SelectContext(ctx, &res, - makeGetAllQuery(condition, ordering, cursor > 0), - account, limit, cursor, block, - ) - - if err != nil { - return nil, pgutil.CheckNoRows(err, payment.ErrNotFound) - } - if len(res) == 0 { - return nil, payment.ErrNotFound - } - - return res, nil -} - -func dbGetAllForAccountByTypeWithinBlockRange(ctx context.Context, db *sqlx.DB, account string, lowerBound, upperBound uint64, cursor uint64, limit uint, ordering q.Ordering, paymentType payment.PaymentType) ([]*model, error) { - res := []*model{} - - var condition string - if paymentType == payment.PaymentType_Send { - condition = "source = $1" - } else { - condition = "destination = $1" - } - - condition += " AND block_id > $4 AND block_id < $5" - - err := db.SelectContext(ctx, &res, - makeGetAllQuery(condition, ordering, cursor > 0), - account, limit, cursor, lowerBound, upperBound, - ) - - if err != nil { - return nil, pgutil.CheckNoRows(err, payment.ErrNotFound) - } - if len(res) == 0 { - return nil, payment.ErrNotFound - } - - return res, nil -} - -func dbGetAllExternalDepositsAfterBlock(ctx context.Context, db *sqlx.DB, account string, block uint64, cursor uint64, limit uint, ordering q.Ordering) ([]*model, error) { - res := []*model{} - - condition := "destination = $1 AND block_id > $4 AND is_external" - - err := db.SelectContext(ctx, &res, - makeGetAllQuery(condition, ordering, cursor > 0), - account, limit, cursor, block, - ) - - if err != nil { - return nil, pgutil.CheckNoRows(err, payment.ErrNotFound) - } - if len(res) == 0 { - return nil, payment.ErrNotFound - } - - return res, nil -} - -func dbGetExternalDepositAmount(ctx context.Context, db *sqlx.DB, account string) (uint64, error) { - var res sql.NullInt64 - - query := `SELECT SUM(quantity) FROM ` + tableName + ` - WHERE destination = $1 AND is_external AND confirmation_state = 3` - - err := db.GetContext(ctx, &res, query, account) - if err != nil { - return 0, err - } - - if !res.Valid { - return 0, nil - } - return uint64(res.Int64), nil -} diff --git a/pkg/code/data/payment/postgres/store.go b/pkg/code/data/payment/postgres/store.go deleted file mode 100644 index acfa8ae8..00000000 --- a/pkg/code/data/payment/postgres/store.go +++ /dev/null @@ -1,125 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/payment" - "github.com/jmoiron/sqlx" -) - -type store struct { - db *sqlx.DB -} - -func New(db *sql.DB) payment.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -func (s *store) Get(ctx context.Context, txId string, index uint32) (*payment.Record, error) { - obj, err := dbGet(ctx, s.db, txId, index) - if err != nil { - return nil, err - } - - return fromModel(obj), nil -} - -func (s *store) Put(ctx context.Context, data *payment.Record) error { - return toModel(data).dbSave(ctx, s.db) -} - -func (s *store) Update(ctx context.Context, data *payment.Record) error { - return toModel(data).dbUpdate(ctx, s.db) -} - -func (s *store) GetAllForTransaction(ctx context.Context, txId string) ([]*payment.Record, error) { - list, err := dbGetAllForTransaction(ctx, s.db, txId) - if err != nil { - return nil, err - } - - res := []*payment.Record{} - for _, item := range list { - res = append(res, fromModel(item)) - } - - return res, nil -} - -func (s *store) GetAllForAccount(ctx context.Context, account string, cursor uint64, limit uint, ordering query.Ordering) ([]*payment.Record, error) { - list, err := dbGetAllForAccount(ctx, s.db, account, cursor, limit, ordering) - if err != nil { - return nil, err - } - - res := []*payment.Record{} - for _, item := range list { - res = append(res, fromModel(item)) - } - - return res, nil -} - -func (s *store) GetAllForAccountByType(ctx context.Context, account string, cursor uint64, limit uint, ordering query.Ordering, paymentType payment.PaymentType) ([]*payment.Record, error) { - list, err := dbGetAllForAccountByType(ctx, s.db, account, cursor, limit, ordering, paymentType) - if err != nil { - return nil, err - } - - res := []*payment.Record{} - for _, item := range list { - res = append(res, fromModel(item)) - } - - return res, nil -} - -func (s *store) GetAllForAccountByTypeAfterBlock(ctx context.Context, account string, block uint64, cursor uint64, limit uint, ordering query.Ordering, paymentType payment.PaymentType) ([]*payment.Record, error) { - list, err := dbGetAllForAccountByTypeAfterBlock(ctx, s.db, account, block, cursor, limit, ordering, paymentType) - if err != nil { - return nil, err - } - - res := []*payment.Record{} - for _, item := range list { - res = append(res, fromModel(item)) - } - - return res, nil -} - -func (s *store) GetAllForAccountByTypeWithinBlockRange(ctx context.Context, account string, lowerBound, upperBound uint64, cursor uint64, limit uint, ordering query.Ordering, paymentType payment.PaymentType) ([]*payment.Record, error) { - list, err := dbGetAllForAccountByTypeWithinBlockRange(ctx, s.db, account, lowerBound, upperBound, cursor, limit, ordering, paymentType) - if err != nil { - return nil, err - } - - res := []*payment.Record{} - for _, item := range list { - res = append(res, fromModel(item)) - } - - return res, nil -} - -func (s *store) GetAllExternalDepositsAfterBlock(ctx context.Context, account string, block uint64, cursor uint64, limit uint, ordering query.Ordering) ([]*payment.Record, error) { - list, err := dbGetAllExternalDepositsAfterBlock(ctx, s.db, account, block, cursor, limit, ordering) - if err != nil { - return nil, err - } - - res := []*payment.Record{} - for _, item := range list { - res = append(res, fromModel(item)) - } - - return res, nil -} - -func (s *store) GetExternalDepositAmount(ctx context.Context, account string) (uint64, error) { - return dbGetExternalDepositAmount(ctx, s.db, account) -} diff --git a/pkg/code/data/payment/postgres/store_test.go b/pkg/code/data/payment/postgres/store_test.go deleted file mode 100644 index af859350..00000000 --- a/pkg/code/data/payment/postgres/store_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/payment" - "github.com/code-payments/code-server/pkg/code/data/payment/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_payment ( - id serial NOT NULL PRIMARY KEY, - - block_id bigint NULL, - block_time timestamp with time zone NULL, - transaction_id text NOT NULL, - transaction_index integer NOT NULL, - rendezvous_key text, - is_external boolean NOT NULL default false, - - source text NOT NULL, - destination text NOT NULL, - quantity bigint NOT NULL CHECK (quantity >= 0), - - exchange_currency varchar(3) NOT NULL, - region varchar(2), - exchange_rate numeric(18, 9) NOT NULL, - usd_market_value numeric(18, 9) NOT NULL, - - is_withdraw BOOL NOT NULL, - - confirmation_state integer NULL, - created_at timestamp with time zone NOT NULL, - - CONSTRAINT codewallet__core_payment__uniq__tx_sig__and__index UNIQUE (transaction_id, transaction_index), - CONSTRAINT codewallet__core_payment__currency_code CHECK (exchange_currency::text ~ '^[a-z]{3}$') - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_payment; - ` -) - -var ( - testStore payment.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestPaymentPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/payment/store.go b/pkg/code/data/payment/store.go deleted file mode 100644 index 558f5410..00000000 --- a/pkg/code/data/payment/store.go +++ /dev/null @@ -1,68 +0,0 @@ -package payment - -import ( - "context" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/pkg/errors" -) - -var ( - ErrNotFound = errors.New("no records could be found") - ErrExists = errors.New("the transaction index for this signature already exists") -) - -type Store interface { - // Get finds the record for a given id - // - // ErrNotFound is returned if the record cannot be found - Get(ctx context.Context, txId string, index uint32) (*Record, error) - - // GetAllForTransaction returns payment records in the store for a - // given transaction signature. - // - // ErrNotFound is returned if no rows are found. - GetAllForTransaction(ctx context.Context, txId string) ([]*Record, error) - - // GetAllForAccount returns payment records in the store for a - // given "account" after a provided "cursor" value and limited to at most - // "limit" results. - // - // ErrNotFound is returned if no rows are found. - GetAllForAccount(ctx context.Context, account string, cursor uint64, limit uint, ordering query.Ordering) ([]*Record, error) - - // GetAllForAccountByType returns payment records in the store for a - // given "account" after a provided "cursor" value and limited to at most - // "limit" results. - // - // ErrNotFound is returned if no rows are found. - GetAllForAccountByType(ctx context.Context, account string, cursor uint64, limit uint, ordering query.Ordering, paymentType PaymentType) ([]*Record, error) - - // GetAllForAccountByTypeAfterBlock returns payment records in the store for a - // given "account" after a "block" after a provided "cursor" value and limited - // to at most "limit" results. - // - // ErrNotFound is returned if no rows are found. - GetAllForAccountByTypeAfterBlock(ctx context.Context, account string, block uint64, cursor uint64, limit uint, ordering query.Ordering, paymentType PaymentType) ([]*Record, error) - - // GetAllForAccountByTypeWithinBlockRange returns payment records in the store - // for a given "account" within a "block" range (lowerBound, upperBOund) after a - // provided "cursor" value and limited to at most "limit" results. - // - // ErrNotFound is returned if no rows are found. - GetAllForAccountByTypeWithinBlockRange(ctx context.Context, account string, lowerBound, upperBound uint64, cursor uint64, limit uint, ordering query.Ordering, paymentType PaymentType) ([]*Record, error) - - // GetExternalDepositAmount gets the total amount of Kin in quarks deposited to - // an account via a deposit from an external account. - GetExternalDepositAmount(ctx context.Context, account string) (uint64, error) - - // Put saves payment metadata to the store. - // - // ErrTransactionIndexExists is returned if a transaction with the same signature already exists. - Put(ctx context.Context, record *Record) error - - // Update an existing record on the backend store/database - // - // ErrNotFound is returned if the record cannot be found - Update(ctx context.Context, record *Record) error -} diff --git a/pkg/code/data/payment/tests/tests.go b/pkg/code/data/payment/tests/tests.go deleted file mode 100644 index d354a4f1..00000000 --- a/pkg/code/data/payment/tests/tests.go +++ /dev/null @@ -1,148 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/payment" -) - -type TestData struct { - Timestamp time.Time - Source string - Destination string - - ExchangeRate float64 - ExchangeCurrency string - - Quantity uint64 - BookCost float64 - MarketValue float64 - UnrealizedGains float64 -} - -func CompareTestDataToPayment(t *testing.T, expected *TestData, actual *payment.Record) { - assert.Equal(t, expected.Timestamp.Unix(), actual.CreatedAt.Unix()) - assert.Equal(t, expected.Source, actual.Source) - assert.Equal(t, expected.Destination, actual.Destination) - assert.Equal(t, expected.ExchangeCurrency, actual.ExchangeCurrency) - assert.Equal(t, expected.ExchangeRate, actual.ExchangeRate) - assert.Equal(t, expected.Quantity, actual.Quantity) - assert.Equal(t, expected.MarketValue, actual.UsdMarketValue) -} - -func RunTests(t *testing.T, s payment.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s payment.Store){ - testRoundTrip, - testGetRange, - } { - tf(t, s) - teardown() - } -} - -func testRoundTrip(t *testing.T, s payment.Store) { - now := time.Now() - region := "ca" - data := &payment.Record{ - TransactionId: "tx_sig", - TransactionIndex: 0, - Source: "source", - Destination: "destination", - Quantity: 100, - ExchangeCurrency: "cad", - Region: ®ion, - ExchangeRate: 100.0, - IsWithdraw: true, - CreatedAt: now, - } - - // Test ErrTransactionSignatureAndIndexNotFound - actualData, err := s.GetAllForAccount(context.Background(), "foobar", 0, 1, query.Ascending) - //actualData, err := s.GetBySignature(context.Background(), "foobar", 0) - assert.Equal(t, payment.ErrNotFound, err) - assert.Nil(t, actualData) - - require.NoError(t, s.Put(context.Background(), data)) - - // Test Err - assert.Equal(t, payment.ErrExists, s.Put(context.Background(), data)) - - actualData, err = s.GetAllForAccount(context.Background(), data.Source, 0, 1, query.Ascending) - //actualData, err = s.GetBySignature(context.Background(), data.TransactionSignature, data.TransactionIndex) - require.NoError(t, err) - assert.Equal(t, 1, len(actualData)) - assert.Equal(t, data.TransactionId, actualData[0].TransactionId) - assert.Equal(t, data.TransactionIndex, actualData[0].TransactionIndex) - assert.Equal(t, data.Source, actualData[0].Source) - assert.Equal(t, data.Destination, actualData[0].Destination) - assert.Equal(t, data.Quantity, actualData[0].Quantity) - assert.Equal(t, data.ExchangeCurrency, actualData[0].ExchangeCurrency) - assert.Equal(t, *data.Region, *actualData[0].Region) - assert.Equal(t, data.ExchangeRate, actualData[0].ExchangeRate) - assert.Equal(t, data.IsWithdraw, actualData[0].IsWithdraw) - assert.Equal(t, data.CreatedAt.Unix(), actualData[0].CreatedAt.Unix()) -} - -func testGetRange(t *testing.T, s payment.Store) { - joe := "joe" - eric := "eric" - tim := "tim" - emma := "emma" - sub := "sub" - - now := time.Now() - times := []time.Time{ - now.Add(-27 * time.Minute), - now.Add(-18 * time.Minute), - now.Add(-10 * time.Minute), - now.Add(-5 * time.Minute), - now.Add(-3 * time.Minute), - now, - } - - testPayments := []*TestData{ - {Timestamp: times[0], Source: eric, Destination: joe, ExchangeCurrency: "cad", ExchangeRate: 0.0000483, Quantity: 103498, BookCost: 5.00, MarketValue: 5.00, UnrealizedGains: 0.00}, - {Timestamp: times[1], Source: joe, Destination: eric, ExchangeCurrency: "cad", ExchangeRate: 0.0000661, Quantity: 88358, BookCost: 4.00, MarketValue: 5.84, UnrealizedGains: 1.84}, - {Timestamp: times[2], Source: tim, Destination: joe, ExchangeCurrency: "cad", ExchangeRate: 0.0000611, Quantity: 248175, BookCost: 13.76, MarketValue: 15.16, UnrealizedGains: 1.40}, - {Timestamp: times[3], Source: joe, Destination: emma, ExchangeCurrency: "cad", ExchangeRate: 0.0000662, Quantity: 184239, BookCost: 9.53, MarketValue: 12.19, UnrealizedGains: 2.66}, - {Timestamp: times[4], Source: sub, Destination: joe, ExchangeCurrency: "cad", ExchangeRate: 0.0000935, Quantity: 317886, BookCost: 22.03, MarketValue: 29.73, UnrealizedGains: 7.70}, - {Timestamp: times[5], Source: sub, Destination: joe, ExchangeCurrency: "cad", ExchangeRate: 0.0001286, Quantity: 415064, BookCost: 34.53, MarketValue: 53.39, UnrealizedGains: 18.86}, - } - - for index, item := range testPayments { - err := s.Put(context.Background(), &payment.Record{ - TransactionIndex: 0, - TransactionId: fmt.Sprintf("tx_sig__%d", index), - - Source: item.Source, // The source account id for this payment - Destination: item.Destination, // The destination account id for this payment - Quantity: item.Quantity, // The amount of Kin (in Quarks) - - ExchangeCurrency: item.ExchangeCurrency, // The (external) agreed upon currency for the exchange - ExchangeRate: item.ExchangeRate, // The (external) agreed upon exchange rate for determining the amount of Kin to transfer - - UsdMarketValue: item.MarketValue, // Not accurate, but fine for testing - - CreatedAt: item.Timestamp, - }) - - require.NoError(t, err) - } - - // GetAllForAccountWithCursor Tests - // -------------------------------------------------------------------------------- - - results, err := s.GetAllForAccount(context.Background(), joe, 4, 100, query.Ascending) - require.NoError(t, err) - assert.Equal(t, 2, len(results)) - CompareTestDataToPayment(t, testPayments[4], results[0]) - CompareTestDataToPayment(t, testPayments[5], results[1]) - -} diff --git a/pkg/code/data/paymentrequest/tests/tests.go b/pkg/code/data/paymentrequest/tests/tests.go index ec17f495..ff680f3a 100644 --- a/pkg/code/data/paymentrequest/tests/tests.go +++ b/pkg/code/data/paymentrequest/tests/tests.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/code-payments/code-server/pkg/code/data/paymentrequest" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" ) @@ -54,7 +53,7 @@ func testRoundTrip(t *testing.T, s paymentrequest.Store) { ExchangeCurrency: pointer.String("usd"), NativeAmount: pointer.Float64(2.46), ExchangeRate: pointer.Float64(1.23), - Quantity: pointer.Uint64(kin.ToQuarks(2)), + Quantity: pointer.Uint64(2), Fees: fees, Domain: pointer.String("example.com"), IsVerified: true, @@ -85,7 +84,7 @@ func testInvalidRecord(t *testing.T, s paymentrequest.Store) { ExchangeCurrency: pointer.String("usd"), NativeAmount: pointer.Float64(2.46), ExchangeRate: pointer.Float64(1.23), - Quantity: pointer.Uint64(kin.ToQuarks(2)), + Quantity: pointer.Uint64(2), Fees: []*paymentrequest.Fee{ { DestinationTokenAccount: "destination2", diff --git a/pkg/code/data/paywall/memory/store.go b/pkg/code/data/paywall/memory/store.go deleted file mode 100644 index 0d6d8a45..00000000 --- a/pkg/code/data/paywall/memory/store.go +++ /dev/null @@ -1,90 +0,0 @@ -package memory - -import ( - "context" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/code/data/paywall" -) - -type store struct { - mu sync.Mutex - records []*paywall.Record - last uint64 -} - -func New() paywall.Store { - return &store{ - records: make([]*paywall.Record, 0), - last: 0, - } -} - -func (s *store) reset() { - s.mu.Lock() - s.records = make([]*paywall.Record, 0) - s.last = 0 - s.mu.Unlock() -} - -// Put implements paywall.Store.Put -func (s *store) Put(_ context.Context, data *paywall.Record) error { - if err := data.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - if item := s.find(data); item != nil { - return paywall.ErrPaywallExists - } else { - if data.Id == 0 { - data.Id = s.last - } - if data.CreatedAt.IsZero() { - data.CreatedAt = time.Now() - } - c := data.Clone() - s.records = append(s.records, &c) - } - - return nil -} - -// GetByShortPath implements paywall.Store.GetByShortPath -func (s *store) GetByShortPath(_ context.Context, path string) (*paywall.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findByShortPath(path) - if item == nil { - return nil, paywall.ErrPaywallNotFound - } - - cloned := item.Clone() - return &cloned, nil -} - -func (s *store) find(data *paywall.Record) *paywall.Record { - for _, item := range s.records { - if item.Id == data.Id { - return item - } - if item.ShortPath == data.ShortPath { - return item - } - } - return nil -} - -func (s *store) findByShortPath(path string) *paywall.Record { - for _, item := range s.records { - if item.ShortPath == path { - return item - } - } - return nil -} diff --git a/pkg/code/data/paywall/memory/store_test.go b/pkg/code/data/paywall/memory/store_test.go deleted file mode 100644 index bb39adb0..00000000 --- a/pkg/code/data/paywall/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/paywall/tests" -) - -func TestPaywallMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/paywall/postgres/model.go b/pkg/code/data/paywall/postgres/model.go deleted file mode 100644 index ea17bcc0..00000000 --- a/pkg/code/data/paywall/postgres/model.go +++ /dev/null @@ -1,111 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/currency" - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - "github.com/code-payments/code-server/pkg/code/data/paywall" -) - -const ( - tableName = "codewallet__core_paywall" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - - OwnerAccount string `db:"owner_account"` - DestinationTokenAccount string `db:"destination_token_account"` - - ExchangeCurrency string `db:"exchange_currency"` - NativeAmount float64 `db:"native_amount"` - RedirectUrl string `db:"redirect_url"` - ShortPath string `db:"short_path"` - - Signature string `db:"signature"` - - CreatedAt time.Time `db:"created_at"` -} - -func toModel(obj *paywall.Record) (*model, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - if obj.CreatedAt.IsZero() { - obj.CreatedAt = time.Now().UTC() - } - - return &model{ - Id: sql.NullInt64{Int64: int64(obj.Id), Valid: true}, - OwnerAccount: obj.OwnerAccount, - DestinationTokenAccount: obj.DestinationTokenAccount, - ExchangeCurrency: string(obj.ExchangeCurrency), - NativeAmount: obj.NativeAmount, - RedirectUrl: obj.RedirectUrl, - ShortPath: obj.ShortPath, - Signature: obj.Signature, - CreatedAt: obj.CreatedAt, - }, nil -} - -func fromModel(obj *model) *paywall.Record { - return &paywall.Record{ - Id: uint64(obj.Id.Int64), - OwnerAccount: obj.OwnerAccount, - DestinationTokenAccount: obj.DestinationTokenAccount, - ExchangeCurrency: currency.Code(obj.ExchangeCurrency), - NativeAmount: obj.NativeAmount, - RedirectUrl: obj.RedirectUrl, - ShortPath: obj.ShortPath, - Signature: obj.Signature, - CreatedAt: obj.CreatedAt, - } -} - -func (m *model) dbPut(ctx context.Context, db *sqlx.DB) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `INSERT INTO ` + tableName + ` - (owner_account, destination_token_account, exchange_currency, native_amount, redirect_url, short_path, signature, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - RETURNING id, owner_account, destination_token_account, exchange_currency, native_amount, redirect_url, short_path, signature, created_at` - - err := tx.QueryRowxContext( - ctx, - query, - m.OwnerAccount, - m.DestinationTokenAccount, - m.ExchangeCurrency, - m.NativeAmount, - m.RedirectUrl, - m.ShortPath, - m.Signature, - m.CreatedAt, - ).StructScan(m) - - return pgutil.CheckUniqueViolation(err, paywall.ErrPaywallExists) - }) -} - -func dbGetByShortPath(ctx context.Context, db *sqlx.DB, path string) (*model, error) { - res := &model{} - - query := `SELECT id, owner_account, destination_token_account, exchange_currency, native_amount, redirect_url, short_path, signature, created_at FROM ` + tableName + ` - WHERE short_path = $1` - - err := db.GetContext( - ctx, - res, - query, - path, - ) - if err != nil { - return nil, pgutil.CheckNoRows(err, paywall.ErrPaywallNotFound) - } - return res, nil -} diff --git a/pkg/code/data/paywall/postgres/store.go b/pkg/code/data/paywall/postgres/store.go deleted file mode 100644 index 29990e63..00000000 --- a/pkg/code/data/paywall/postgres/store.go +++ /dev/null @@ -1,47 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/paywall" -) - -type store struct { - db *sqlx.DB -} - -func New(db *sql.DB) paywall.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Put implements paywall.Store.Put -func (s *store) Put(ctx context.Context, record *paywall.Record) error { - m, err := toModel(record) - if err != nil { - return err - } - - err = m.dbPut(ctx, s.db) - if err != nil { - return err - } - - res := fromModel(m) - res.CopyTo(record) - - return nil -} - -// GetByShortPath implements paywall.Store.GetByShortPath -func (s *store) GetByShortPath(ctx context.Context, path string) (*paywall.Record, error) { - m, err := dbGetByShortPath(ctx, s.db, path) - if err != nil { - return nil, err - } - return fromModel(m), nil -} diff --git a/pkg/code/data/paywall/postgres/store_test.go b/pkg/code/data/paywall/postgres/store_test.go deleted file mode 100644 index f2b27721..00000000 --- a/pkg/code/data/paywall/postgres/store_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/paywall" - "github.com/code-payments/code-server/pkg/code/data/paywall/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_paywall( - id SERIAL NOT NULL PRIMARY KEY, - - owner_account TEXT NOT NULL, - destination_token_account TEXT NOT NULL, - - exchange_currency VARCHAR(3) NOT NULL, - native_amount NUMERIC(18, 9) NOT NULL, - redirect_url TEXT NOT NULL, - short_path TEXT NOT NULL UNIQUE, - - signature TEXT NOT NULL, - - created_at TIMESTAMP WITH TIME ZONE NOT NULL - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_paywall; - ` -) - -var ( - testStore paywall.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestPaywallPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/paywall/record.go b/pkg/code/data/paywall/record.go deleted file mode 100644 index d350d91b..00000000 --- a/pkg/code/data/paywall/record.go +++ /dev/null @@ -1,90 +0,0 @@ -package paywall - -import ( - "errors" - "time" - - "github.com/code-payments/code-server/pkg/currency" -) - -type Record struct { - Id uint64 - - OwnerAccount string - DestinationTokenAccount string - - ExchangeCurrency currency.Code - NativeAmount float64 - RedirectUrl string - ShortPath string - - Signature string - - CreatedAt time.Time -} - -func (r *Record) Validate() error { - if len(r.OwnerAccount) == 0 { - return errors.New("owner account is required") - } - - if len(r.DestinationTokenAccount) == 0 { - return errors.New("destination token account is required") - } - - if len(r.ExchangeCurrency) == 0 { - return errors.New("exchange currency is required") - } - - if r.NativeAmount == 0 { - return errors.New("native amount cannot be zero") - } - - if len(r.RedirectUrl) == 0 { - return errors.New("redirect url is required") - } - - if len(r.ShortPath) == 0 { - return errors.New("short path is required") - } - - if len(r.Signature) == 0 { - return errors.New("signature is required") - } - - return nil -} - -func (r *Record) Clone() Record { - return Record{ - Id: r.Id, - - OwnerAccount: r.OwnerAccount, - DestinationTokenAccount: r.DestinationTokenAccount, - - ExchangeCurrency: r.ExchangeCurrency, - NativeAmount: r.NativeAmount, - RedirectUrl: r.RedirectUrl, - ShortPath: r.ShortPath, - - Signature: r.Signature, - - CreatedAt: r.CreatedAt, - } -} - -func (r *Record) CopyTo(dst *Record) { - dst.Id = r.Id - - dst.OwnerAccount = r.OwnerAccount - dst.DestinationTokenAccount = r.DestinationTokenAccount - - dst.ExchangeCurrency = r.ExchangeCurrency - dst.NativeAmount = r.NativeAmount - dst.RedirectUrl = r.RedirectUrl - dst.ShortPath = r.ShortPath - - dst.Signature = r.Signature - - dst.CreatedAt = r.CreatedAt -} diff --git a/pkg/code/data/paywall/store.go b/pkg/code/data/paywall/store.go deleted file mode 100644 index 3545ee07..00000000 --- a/pkg/code/data/paywall/store.go +++ /dev/null @@ -1,17 +0,0 @@ -package paywall - -import ( - "context" - "errors" -) - -var ( - ErrPaywallNotFound = errors.New("paywall record not found") - ErrPaywallExists = errors.New("paywall record already exists") -) - -type Store interface { - Put(ctx context.Context, record *Record) error - - GetByShortPath(ctx context.Context, path string) (*Record, error) -} diff --git a/pkg/code/data/paywall/tests/tests.go b/pkg/code/data/paywall/tests/tests.go deleted file mode 100644 index 3c16abfb..00000000 --- a/pkg/code/data/paywall/tests/tests.go +++ /dev/null @@ -1,65 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/paywall" -) - -func RunTests(t *testing.T, s paywall.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s paywall.Store){ - testRoundTrip, - } { - tf(t, s) - teardown() - } -} - -func testRoundTrip(t *testing.T, s paywall.Store) { - t.Run("testRoundTrip", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.GetByShortPath(ctx, "abcd1234") - require.Error(t, err) - assert.Equal(t, paywall.ErrPaywallNotFound, err) - assert.Nil(t, actual) - - expected := &paywall.Record{ - OwnerAccount: "owner", - DestinationTokenAccount: "destination", - ExchangeCurrency: "usd", - NativeAmount: 0.25, - RedirectUrl: "http://redirect.to/me", - ShortPath: "abcd1234", - Signature: "signature", - CreatedAt: time.Now(), - } - cloned := expected.Clone() - err = s.Put(ctx, expected) - require.NoError(t, err) - - assert.Equal(t, paywall.ErrPaywallExists, s.Put(ctx, expected)) - require.NoError(t, err) - - actual, err = s.GetByShortPath(ctx, "abcd1234") - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - assert.EqualValues(t, 1, actual.Id) - }) -} - -func assertEquivalentRecords(t *testing.T, obj1, obj2 *paywall.Record) { - assert.Equal(t, obj1.OwnerAccount, obj2.OwnerAccount) - assert.Equal(t, obj1.DestinationTokenAccount, obj2.DestinationTokenAccount) - assert.Equal(t, obj1.ExchangeCurrency, obj2.ExchangeCurrency) - assert.Equal(t, obj1.NativeAmount, obj2.NativeAmount) - assert.Equal(t, obj1.RedirectUrl, obj2.RedirectUrl) - assert.Equal(t, obj1.ShortPath, obj2.ShortPath) - assert.Equal(t, obj1.Signature, obj2.Signature) - assert.Equal(t, obj1.CreatedAt.Unix(), obj2.CreatedAt.Unix()) -} diff --git a/pkg/code/data/phone/memory/store.go b/pkg/code/data/phone/memory/store.go deleted file mode 100644 index 2404d16f..00000000 --- a/pkg/code/data/phone/memory/store.go +++ /dev/null @@ -1,390 +0,0 @@ -package memory - -import ( - "context" - "sort" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/code/data/phone" -) - -type store struct { - mu sync.RWMutex - - verificationsByAccount map[string][]*phone.Verification - verificationsByNumber map[string][]*phone.Verification - - linkingTokensByNumber map[string]*phone.LinkingToken - - settingsByNumber map[string]*phone.Settings - - eventsByNumber map[string][]*phone.Event - eventsByVerification map[string][]*phone.Event -} - -// New returns an in memory phone.Store. -func New() phone.Store { - return &store{ - verificationsByAccount: make(map[string][]*phone.Verification), - verificationsByNumber: make(map[string][]*phone.Verification), - - linkingTokensByNumber: make(map[string]*phone.LinkingToken), - - settingsByNumber: make(map[string]*phone.Settings), - - eventsByNumber: make(map[string][]*phone.Event), - eventsByVerification: make(map[string][]*phone.Event), - } -} - -// SaveVerification implements phone.Store.SaveVerification -func (s *store) SaveVerification(ctx context.Context, newVerification *phone.Verification) error { - if err := newVerification.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - var alreadyExists bool - currentByAccount := s.verificationsByAccount[newVerification.OwnerAccount] - for _, verification := range currentByAccount { - if verification.OwnerAccount != newVerification.OwnerAccount || verification.PhoneNumber != newVerification.PhoneNumber { - continue - } - - if !newVerification.LastVerifiedAt.After(verification.LastVerifiedAt) { - return phone.ErrInvalidVerification - } - - verification.LastVerifiedAt = newVerification.LastVerifiedAt - - alreadyExists = true - break - } - if !alreadyExists { - copy := &phone.Verification{ - OwnerAccount: newVerification.OwnerAccount, - PhoneNumber: newVerification.PhoneNumber, - CreatedAt: newVerification.CreatedAt, - LastVerifiedAt: newVerification.LastVerifiedAt, - } - s.verificationsByAccount[copy.OwnerAccount] = append(s.verificationsByAccount[copy.OwnerAccount], copy) - s.verificationsByNumber[copy.PhoneNumber] = append(s.verificationsByNumber[copy.PhoneNumber], copy) - } - - currentByAccount = s.verificationsByAccount[newVerification.OwnerAccount] - sort.Slice(currentByAccount, func(i, j int) bool { - return currentByAccount[i].LastVerifiedAt.After(currentByAccount[j].LastVerifiedAt) - }) - - currentByNumber := s.verificationsByNumber[newVerification.PhoneNumber] - sort.Slice(currentByNumber, func(i, j int) bool { - return currentByNumber[i].LastVerifiedAt.After(currentByNumber[j].LastVerifiedAt) - }) - - return nil -} - -// GetVerification implements phone.Store.GetVerification -func (s *store) GetVerification(ctx context.Context, account, phoneNumber string) (*phone.Verification, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - verifications, ok := s.verificationsByAccount[account] - if !ok { - return nil, phone.ErrVerificationNotFound - } - - for _, verification := range verifications { - if verification.PhoneNumber == phoneNumber { - return verification, nil - } - } - - return nil, phone.ErrVerificationNotFound -} - -// GetLatestVerificationForAccount implements phone.Store.GetLatestVerificationForAccount -func (s *store) GetLatestVerificationForAccount(ctx context.Context, account string) (*phone.Verification, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - verifications, ok := s.verificationsByAccount[account] - if !ok { - return nil, phone.ErrVerificationNotFound - } - - return verifications[0], nil -} - -// GetLatestVerificationForNumber implements phone.Store.GetLatestVerificationForNumber -func (s *store) GetLatestVerificationForNumber(ctx context.Context, phoneNumber string) (*phone.Verification, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - verifications, ok := s.verificationsByNumber[phoneNumber] - if !ok { - return nil, phone.ErrVerificationNotFound - } - - return verifications[0], nil -} - -// GetAllVerificationsForNumber implements phone.Store.GetAllVerificationsForNumber -func (s *store) GetAllVerificationsForNumber(ctx context.Context, phoneNumber string) ([]*phone.Verification, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - verifications, ok := s.verificationsByNumber[phoneNumber] - if !ok { - return nil, phone.ErrVerificationNotFound - } - - return verifications, nil -} - -// SaveLinkingToken implements phone.Store.SaveLinkingToken -func (s *store) SaveLinkingToken(ctx context.Context, token *phone.LinkingToken) error { - if err := token.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - copy := &phone.LinkingToken{ - PhoneNumber: token.PhoneNumber, - Code: token.Code, - ExpiresAt: token.ExpiresAt, - CurrentCheckCount: token.CurrentCheckCount, - MaxCheckCount: token.MaxCheckCount, - } - s.linkingTokensByNumber[copy.PhoneNumber] = copy - - return nil -} - -// UseLinkingToken implements phone.Store.UseLinkingToken -func (s *store) UseLinkingToken(ctx context.Context, phoneNumber, code string) error { - s.mu.Lock() - defer s.mu.Unlock() - - token, ok := s.linkingTokensByNumber[phoneNumber] - if !ok { - return phone.ErrLinkingTokenNotFound - } - - token.CurrentCheckCount++ - - if token.Code != code { - return phone.ErrLinkingTokenNotFound - } - - delete(s.linkingTokensByNumber, phoneNumber) - - if !ok || token.ExpiresAt.Before(time.Now()) || token.CurrentCheckCount > token.MaxCheckCount { - return phone.ErrLinkingTokenNotFound - } - - return nil -} - -// FilterVerifiedNumbers implements phone.Store.FilterVerifiedNumbers -func (s *store) FilterVerifiedNumbers(ctx context.Context, phoneNumbers []string) ([]string, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - var filtered []string - for _, phoneNumber := range phoneNumbers { - _, ok := s.verificationsByNumber[phoneNumber] - if ok { - filtered = append(filtered, phoneNumber) - } - } - - return filtered, nil -} - -// GetSettings implements phone.Store.GetSettings -func (s *store) GetSettings(ctx context.Context, phoneNumber string) (*phone.Settings, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - settings, ok := s.settingsByNumber[phoneNumber] - if !ok { - return &phone.Settings{ - PhoneNumber: phoneNumber, - }, nil - } - - return settings, nil -} - -// SaveOwnerAccountSetting implements phone.Store.SaveOwnerAccountSetting -func (s *store) SaveOwnerAccountSetting(ctx context.Context, phoneNumber string, newSettings *phone.OwnerAccountSetting) error { - if err := newSettings.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - copy := &phone.Settings{ - PhoneNumber: phoneNumber, - ByOwnerAccount: make(map[string]*phone.OwnerAccountSetting), - } - - phoneSettings, ok := s.settingsByNumber[phoneNumber] - if ok { - copy = phoneSettings - } - - currentSettings, ok := copy.ByOwnerAccount[newSettings.OwnerAccount] - if ok { - if newSettings.IsUnlinked != nil { - flagCopy := *newSettings.IsUnlinked - currentSettings.IsUnlinked = &flagCopy - } - currentSettings.LastUpdatedAt = newSettings.LastUpdatedAt - return nil - } - - copy.ByOwnerAccount[newSettings.OwnerAccount] = &phone.OwnerAccountSetting{ - OwnerAccount: newSettings.OwnerAccount, - IsUnlinked: newSettings.IsUnlinked, - CreatedAt: newSettings.CreatedAt, - LastUpdatedAt: newSettings.LastUpdatedAt, - } - s.settingsByNumber[phoneNumber] = copy - - return nil -} - -// PutEvent implements phone.Store.PutEvent -func (s *store) PutEvent(ctx context.Context, event *phone.Event) error { - if err := event.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - eventsByNumber := s.eventsByNumber[event.PhoneNumber] - eventsByVerification := s.eventsByVerification[event.VerificationId] - - copy := &phone.Event{ - Type: event.Type, - VerificationId: event.VerificationId, - PhoneNumber: event.PhoneNumber, - PhoneMetadata: event.PhoneMetadata, - CreatedAt: event.CreatedAt, - } - - eventsByNumber = append(eventsByNumber, copy) - s.eventsByNumber[event.PhoneNumber] = eventsByNumber - - eventsByVerification = append(eventsByVerification, copy) - s.eventsByVerification[event.VerificationId] = eventsByVerification - - return nil -} - -// GetLatestEventForNumberByType implements phone.Store.GetLatestEventForNumberByType -func (s *store) GetLatestEventForNumberByType(ctx context.Context, phoneNumber string, eventType phone.EventType) (*phone.Event, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - var latestTimestamp time.Time - var res *phone.Event - - for _, event := range s.eventsByNumber[phoneNumber] { - if event.Type != eventType { - continue - } - - if event.CreatedAt.Before(latestTimestamp) { - continue - } - - res = event - latestTimestamp = event.CreatedAt - } - - if res == nil { - return nil, phone.ErrEventNotFound - } - return res, nil -} - -// CountEventsForVerificationByType implements phone.Store.CountEventsForVerificationByType -func (s *store) CountEventsForVerificationByType(ctx context.Context, verification string, eventType phone.EventType) (uint64, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - var count uint64 - - for _, event := range s.eventsByVerification[verification] { - if event.Type == eventType { - count += 1 - } - } - - return count, nil -} - -// CountEventsForNumberByTypeSinceTimestamp implements phone.Store.CountEventsForNumberByTypeSinceTimestamp -func (s *store) CountEventsForNumberByTypeSinceTimestamp(ctx context.Context, phoneNumber string, eventType phone.EventType, since time.Time) (uint64, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - var count uint64 - - for _, event := range s.eventsByNumber[phoneNumber] { - if event.CreatedAt.Before(since) { - continue - } - - if event.Type != eventType { - continue - } - - count += 1 - } - - return count, nil -} - -// CountUniqueVerificationIdsForNumberSinceTimestamp implements phone.Store.CountUniqueVerificationIdsForNumberSinceTimestamp -func (s *store) CountUniqueVerificationIdsForNumberSinceTimestamp(ctx context.Context, phoneNumber string, since time.Time) (uint64, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - res := make(map[string]struct{}) - - for _, event := range s.eventsByNumber[phoneNumber] { - if event.CreatedAt.Before(since) { - continue - } - - res[event.VerificationId] = struct{}{} - } - - return uint64(len(res)), nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.verificationsByAccount = make(map[string][]*phone.Verification) - s.verificationsByNumber = make(map[string][]*phone.Verification) - - s.linkingTokensByNumber = make(map[string]*phone.LinkingToken) - - s.settingsByNumber = make(map[string]*phone.Settings) - - s.eventsByNumber = make(map[string][]*phone.Event) - s.eventsByVerification = make(map[string][]*phone.Event) -} diff --git a/pkg/code/data/phone/memory/stores_test.go b/pkg/code/data/phone/memory/stores_test.go deleted file mode 100644 index 1c031647..00000000 --- a/pkg/code/data/phone/memory/stores_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/phone/tests" -) - -func TestPhoneMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/phone/postgres/model.go b/pkg/code/data/phone/postgres/model.go deleted file mode 100644 index 8263af41..00000000 --- a/pkg/code/data/phone/postgres/model.go +++ /dev/null @@ -1,509 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - "time" - - "github.com/jmoiron/sqlx" - - phoneutil "github.com/code-payments/code-server/pkg/phone" - - "github.com/code-payments/code-server/pkg/code/data/phone" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" -) - -const ( - verificationTableName = "codewallet__core_phoneverification" - linkingTokenTableName = "codewallet__core_phonelinkingtoken" - settingTableName = "codewallet__core_phonesetting" - eventTableName = "codewallet__core_phoneevent" -) - -type verificationModel struct { - Id sql.NullInt64 `db:"id"` - PhoneNumber string `db:"phone_number"` - OwnerAccount string `db:"owner_account"` - CreatedAt time.Time `db:"created_at"` - LastVerifiedAt time.Time `db:"last_verified_at"` -} - -func toVerificationModel(obj *phone.Verification) (*verificationModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &verificationModel{ - PhoneNumber: obj.PhoneNumber, - OwnerAccount: obj.OwnerAccount, - CreatedAt: obj.CreatedAt, - LastVerifiedAt: obj.LastVerifiedAt, - }, nil -} - -func fromVerificationModel(obj *verificationModel) *phone.Verification { - return &phone.Verification{ - PhoneNumber: obj.PhoneNumber, - OwnerAccount: obj.OwnerAccount, - CreatedAt: obj.CreatedAt, - LastVerifiedAt: obj.LastVerifiedAt, - } -} - -func (m *verificationModel) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + verificationTableName + ` - ( - phone_number, owner_account, created_at, last_verified_at - ) - VALUES ($1,$2,$3,$4) - ON CONFLICT (phone_number, owner_account) - DO UPDATE - SET last_verified_at = $4 - WHERE ` + verificationTableName + `.phone_number = $1 AND ` + verificationTableName + `.owner_account = $2 AND ` + verificationTableName + `.last_verified_at < $4 - RETURNING id, phone_number, owner_account, last_verified_at` - - err := db.QueryRowxContext( - ctx, - query, - m.PhoneNumber, - m.OwnerAccount, - m.CreatedAt.UTC(), - m.LastVerifiedAt.UTC(), - ).StructScan(m) - return pgutil.CheckNoRows(err, phone.ErrInvalidVerification) -} - -func dbGetVerification(ctx context.Context, db *sqlx.DB, account, phoneNumber string) (*verificationModel, error) { - res := &verificationModel{} - - query := `SELECT id, phone_number, owner_account, created_at, last_verified_at FROM ` + verificationTableName + ` - WHERE owner_account = $1 AND phone_number = $2 - ` - - err := db.GetContext(ctx, res, query, account, phoneNumber) - if err != nil { - return nil, pgutil.CheckNoRows(err, phone.ErrVerificationNotFound) - } - return res, nil -} - -func dbGetLatestVerificationForAccount(ctx context.Context, db *sqlx.DB, account string) (*verificationModel, error) { - res := &verificationModel{} - - query := `SELECT id, phone_number, owner_account, created_at, last_verified_at FROM ` + verificationTableName + ` - WHERE owner_account = $1 - ORDER BY last_verified_at DESC - LIMIT 1 - ` - - err := db.GetContext(ctx, res, query, account) - if err != nil { - return nil, pgutil.CheckNoRows(err, phone.ErrVerificationNotFound) - } - return res, nil -} - -func dbGetLatestVerificationForNumber(ctx context.Context, db *sqlx.DB, phoneNumber string) (*verificationModel, error) { - res := &verificationModel{} - - query := `SELECT id, phone_number, owner_account, created_at, last_verified_at FROM ` + verificationTableName + ` - WHERE phone_number = $1 - ORDER BY last_verified_at DESC - LIMIT 1 - ` - - err := db.GetContext(ctx, res, query, phoneNumber) - if err != nil { - return nil, pgutil.CheckNoRows(err, phone.ErrVerificationNotFound) - } - return res, nil -} - -func dbGetAllVerificationsForNumber(ctx context.Context, db *sqlx.DB, phoneNumber string) ([]*verificationModel, error) { - var res []*verificationModel - - query := `SELECT id, phone_number, owner_account, created_at, last_verified_at FROM ` + verificationTableName + ` - WHERE phone_number = $1 - ORDER BY last_verified_at DESC - ` - - err := db.SelectContext(ctx, &res, query, phoneNumber) - if err != nil { - return nil, pgutil.CheckNoRows(err, phone.ErrVerificationNotFound) - } - if len(res) == 0 { - return nil, phone.ErrVerificationNotFound - } - return res, nil -} - -type linkingTokenModel struct { - Id sql.NullInt64 `db:"id"` - PhoneNumber string `db:"phone_number"` - Code string `db:"code"` - CurrentCheckCount uint32 `db:"current_check_count"` - MaxCheckCount uint32 `db:"max_check_count"` - ExpiresAt time.Time `db:"expires_at"` -} - -func toLinkingTokenModel(obj *phone.LinkingToken) (*linkingTokenModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &linkingTokenModel{ - PhoneNumber: obj.PhoneNumber, - Code: obj.Code, - CurrentCheckCount: obj.CurrentCheckCount, - MaxCheckCount: obj.MaxCheckCount, - ExpiresAt: obj.ExpiresAt, - }, nil -} - -func fromLinkingTokenModel(obj *linkingTokenModel) *phone.LinkingToken { - return &phone.LinkingToken{ - PhoneNumber: obj.PhoneNumber, - Code: obj.Code, - CurrentCheckCount: uint32(obj.CurrentCheckCount), - MaxCheckCount: uint32(obj.MaxCheckCount), - ExpiresAt: obj.ExpiresAt, - } -} - -func (m *linkingTokenModel) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + linkingTokenTableName + ` - ( - phone_number, code, current_check_count, max_check_count, expires_at - ) - VALUES ($1,$2,$3,$4,$5) - ON CONFLICT (phone_number) - DO UPDATE - SET code = $2, current_check_count = $3, max_check_count = $4, expires_at = $5 - WHERE ` + linkingTokenTableName + `.phone_number = $1 - RETURNING id, phone_number, code, current_check_count, max_check_count, expires_at` - - err := db.QueryRowxContext( - ctx, - query, - m.PhoneNumber, - m.Code, - m.CurrentCheckCount, - m.MaxCheckCount, - m.ExpiresAt.UTC(), - ).StructScan(m) - return err -} - -func dbUseLinkingToken(ctx context.Context, db *sqlx.DB, phoneNumber, code string) error { - res := &linkingTokenModel{} - - query := `UPDATE ` + linkingTokenTableName + ` - SET current_check_count = current_check_count + 1 - WHERE phone_number = $1 - RETURNING id, phone_number, code, current_check_count, max_check_count, expires_at` - - err := db.QueryRowxContext( - ctx, - query, - phoneNumber, - ).StructScan(res) - if err != nil { - return pgutil.CheckNoRows(err, phone.ErrLinkingTokenNotFound) - } - - query = `DELETE FROM ` + linkingTokenTableName + ` - WHERE phone_number = $1 AND code = $2 - RETURNING id, phone_number, code, current_check_count, max_check_count, expires_at` - - err = db.QueryRowxContext( - ctx, - query, - phoneNumber, - code, - ).StructScan(res) - - if err != nil { - return pgutil.CheckNoRows(err, phone.ErrLinkingTokenNotFound) - } - - if res.ExpiresAt.Before(time.Now()) || res.CurrentCheckCount > res.MaxCheckCount { - return phone.ErrLinkingTokenNotFound - } - - return nil -} - -func dbFilterVerifiedNumbers(ctx context.Context, db *sqlx.DB, phoneNumbers []string) ([]string, error) { - if len(phoneNumbers) == 0 { - return nil, nil - } - - phoneNumberArgs := make([]string, len(phoneNumbers)) - for i, phoneNumber := range phoneNumbers { - phoneNumberArgs[i] = fmt.Sprintf("'%s'", phoneNumber) - } - - // todo: is there a better way to construct the query? - query := fmt.Sprintf( - "SELECT DISTINCT phone_number FROM %s WHERE phone_number IN (%s) ORDER BY phone_number ASC", - verificationTableName, - strings.Join(phoneNumberArgs, ","), - ) - - rows, err := db.QueryContext(ctx, query) - if err != nil { - return nil, err - } - - var result []string - - for rows.Next() { - var phoneNumber string - err := rows.Scan(&phoneNumber) - if err != nil { - return nil, err - } - - result = append(result, phoneNumber) - } - - return result, nil -} - -type ownerAccountSettingModel struct { - Id sql.NullInt64 `db:"id"` - PhoneNumber string `db:"phone_number"` - OwnerAccount string `db:"owner_account"` - IsUnlinked sql.NullBool `db:"is_unlinked"` - CreatedAt time.Time `db:"created_at"` - LastUpdatedAt time.Time `db:"last_updated_at"` -} - -func toOwnerAccountSettingModel(phoneNumber string, obj *phone.OwnerAccountSetting) (*ownerAccountSettingModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - if !phoneutil.IsE164Format(phoneNumber) { - return nil, errors.New("phone number doesn't match E.164 standard") - } - - var isUnlinked sql.NullBool - if obj.IsUnlinked != nil { - isUnlinked.Valid = true - isUnlinked.Bool = *obj.IsUnlinked - } - - return &ownerAccountSettingModel{ - PhoneNumber: phoneNumber, - OwnerAccount: obj.OwnerAccount, - IsUnlinked: isUnlinked, - CreatedAt: obj.CreatedAt, - LastUpdatedAt: obj.LastUpdatedAt, - }, nil -} - -func fromOwnerAccountSettingModel(obj *ownerAccountSettingModel) *phone.OwnerAccountSetting { - var isUnlinked *bool - if obj.IsUnlinked.Valid { - isUnlinked = &obj.IsUnlinked.Bool - } - - return &phone.OwnerAccountSetting{ - OwnerAccount: obj.OwnerAccount, - IsUnlinked: isUnlinked, - CreatedAt: obj.CreatedAt, - LastUpdatedAt: obj.LastUpdatedAt, - } -} - -func (m *ownerAccountSettingModel) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + settingTableName + ` - ( - phone_number, owner_account, is_unlinked, created_at, last_updated_at - ) - VALUES ($1,$2,$3,$4,$5) - ON CONFLICT (phone_number, owner_account) - DO UPDATE - SET - is_unlinked = COALESCE($3, ` + settingTableName + `.is_unlinked), - last_updated_at = $5 - WHERE ` + settingTableName + `.phone_number = $1 AND ` + settingTableName + `.owner_account = $2 - RETURNING id, phone_number, owner_account, is_unlinked, created_at, last_updated_at` - - return db.QueryRowxContext( - ctx, - query, - m.PhoneNumber, - m.OwnerAccount, - m.IsUnlinked, - m.CreatedAt.UTC(), - m.LastUpdatedAt.UTC(), - ).StructScan(m) -} - -func dbGetOwnerAccountSettings(ctx context.Context, db *sqlx.DB, phoneNumber string) ([]*ownerAccountSettingModel, error) { - var res []*ownerAccountSettingModel - - query := `SELECT id, phone_number, owner_account, is_unlinked, created_at, last_updated_at FROM ` + settingTableName + ` - WHERE phone_number = $1` - - err := db.SelectContext(ctx, &res, query, phoneNumber) - - return res, pgutil.CheckNoRows(err, nil) -} - -type eventModel struct { - Id sql.NullInt64 `db:"id"` - EventType int `db:"event_type"` - VerificationId string `db:"verification_id"` - PhoneNumber string `db:"phone_number"` - PhoneType sql.NullInt64 `db:"phone_type"` - MobileCountryCode sql.NullInt64 `db:"mobile_country_code"` - MobileNetworkCode sql.NullInt64 `db:"mobile_network_code"` - CreatedAt time.Time `db:"created_at"` -} - -func toEventModel(obj *phone.Event) (*eventModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - var phoneType sql.NullInt64 - if obj.PhoneMetadata.Type != nil { - phoneType.Valid = true - phoneType.Int64 = int64(*obj.PhoneMetadata.Type) - } - - var mobileCountryCode sql.NullInt64 - if obj.PhoneMetadata.MobileCountryCode != nil { - mobileCountryCode.Valid = true - mobileCountryCode.Int64 = int64(*obj.PhoneMetadata.MobileCountryCode) - } - - var mobileNetworkCode sql.NullInt64 - if obj.PhoneMetadata.MobileNetworkCode != nil { - mobileNetworkCode.Valid = true - mobileNetworkCode.Int64 = int64(*obj.PhoneMetadata.MobileNetworkCode) - } - - return &eventModel{ - EventType: int(obj.Type), - VerificationId: obj.VerificationId, - PhoneNumber: obj.PhoneNumber, - PhoneType: phoneType, - MobileCountryCode: mobileCountryCode, - MobileNetworkCode: mobileNetworkCode, - CreatedAt: obj.CreatedAt, - }, nil -} - -func fromEventModel(obj *eventModel) *phone.Event { - var phoneType *phoneutil.Type - if obj.PhoneType.Valid { - value := phoneutil.Type(obj.PhoneType.Int64) - phoneType = &value - } - - var mobileCountryCode *int - if obj.MobileCountryCode.Valid { - value := int(obj.MobileCountryCode.Int64) - mobileCountryCode = &value - } - - var mobileNetworkCode *int - if obj.MobileNetworkCode.Valid { - value := int(obj.MobileNetworkCode.Int64) - mobileNetworkCode = &value - } - - return &phone.Event{ - Type: phone.EventType(obj.EventType), - VerificationId: obj.VerificationId, - PhoneNumber: obj.PhoneNumber, - PhoneMetadata: &phoneutil.Metadata{ - PhoneNumber: obj.PhoneNumber, - Type: phoneType, - MobileCountryCode: mobileCountryCode, - MobileNetworkCode: mobileNetworkCode, - }, - CreatedAt: obj.CreatedAt, - } -} - -func (m *eventModel) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + eventTableName + ` - ( - event_type, verification_id, phone_number, phone_type, mobile_country_code, mobile_network_code, created_at - ) - VALUES ($1,$2,$3,$4,$5,$6,$7) - RETURNING id, event_type, verification_id, phone_number, phone_type, mobile_country_code, mobile_network_code, created_at` - - return db.QueryRowxContext( - ctx, - query, - m.EventType, - m.VerificationId, - m.PhoneNumber, - m.PhoneType, - m.MobileCountryCode, - m.MobileNetworkCode, - m.CreatedAt, - ).StructScan(m) -} - -func dbGetLatestEventForNumberByType(ctx context.Context, db *sqlx.DB, phoneNumber string, eventType phone.EventType) (*eventModel, error) { - res := &eventModel{} - - query := `SELECT id, event_type, verification_id, phone_number, phone_type, mobile_country_code, mobile_network_code, created_at FROM ` + eventTableName + ` - WHERE phone_number = $1 and event_type = $2 - ORDER BY created_at DESC - LIMIT 1 - ` - - err := db.GetContext(ctx, res, query, phoneNumber, eventType) - if err != nil { - return nil, pgutil.CheckNoRows(err, phone.ErrEventNotFound) - } - return res, nil -} - -func dbCountEventsForVerificationByType(ctx context.Context, db *sqlx.DB, verification string, eventType phone.EventType) (uint64, error) { - var res uint64 - - query := `SELECT COUNT(*) FROM ` + eventTableName + ` WHERE verification_id = $1 AND event_type = $2` - err := db.GetContext(ctx, &res, query, verification, eventType) - if err != nil { - return 0, err - } - - return res, nil -} - -func dbCountEventsForNumberByTypeSinceTimestamp(ctx context.Context, db *sqlx.DB, phoneNumber string, eventType phone.EventType, since time.Time) (uint64, error) { - var res uint64 - - query := `SELECT COUNT(*) FROM ` + eventTableName + ` WHERE phone_number = $1 AND event_type = $2 AND created_at >= $3` - err := db.GetContext(ctx, &res, query, phoneNumber, eventType, since) - if err != nil { - return 0, err - } - - return res, nil -} - -func dbCountUniqueVerificationIdsForNumberSinceTimestamp(ctx context.Context, db *sqlx.DB, phoneNumber string, since time.Time) (uint64, error) { - var res uint64 - - query := `SELECT COUNT(DISTINCT verification_id) FROM ` + eventTableName + ` WHERE phone_number = $1 AND created_at >= $2` - err := db.GetContext(ctx, &res, query, phoneNumber, since) - if err != nil { - return 0, err - } - - return res, nil -} diff --git a/pkg/code/data/phone/postgres/model_test.go b/pkg/code/data/phone/postgres/model_test.go deleted file mode 100644 index a49c5f06..00000000 --- a/pkg/code/data/phone/postgres/model_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package postgres - -import ( - "crypto/ed25519" - "testing" - "time" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - phoneutil "github.com/code-payments/code-server/pkg/phone" - "github.com/code-payments/code-server/pkg/code/data/phone" -) - -func TestVerificationModelConversion(t *testing.T) { - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - verification := &phone.Verification{ - PhoneNumber: "+11234567890", - OwnerAccount: base58.Encode(pub), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now().Add(1 * time.Hour), - } - - model, err := toVerificationModel(verification) - require.NoError(t, err) - - actual := fromVerificationModel(model) - assert.EqualValues(t, verification, actual) -} - -func TestLinkingTokenModelConversion(t *testing.T) { - token := &phone.LinkingToken{ - PhoneNumber: "+11234567890", - Code: "123456", - CurrentCheckCount: 1, - MaxCheckCount: 5, - ExpiresAt: time.Now().Add(1 * time.Hour), - } - - model, err := toLinkingTokenModel(token) - require.NoError(t, err) - - actual := fromLinkingTokenModel(model) - assert.EqualValues(t, token, actual) -} - -func TestOwnerAccountSettingModelConversion(t *testing.T) { - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - phoneNumber := "+11234567890" - - trueVal := true - for _, isUnlinked := range []*bool{nil, &trueVal} { - setting := &phone.OwnerAccountSetting{ - OwnerAccount: base58.Encode(pub), - CreatedAt: time.Now(), - IsUnlinked: isUnlinked, - LastUpdatedAt: time.Now().Add(1 * time.Hour), - } - - model, err := toOwnerAccountSettingModel(phoneNumber, setting) - require.NoError(t, err) - - assert.Equal(t, phoneNumber, model.PhoneNumber) - - actual := fromOwnerAccountSettingModel(model) - assert.EqualValues(t, setting, actual) - } -} - -func TestEventModelConversion(t *testing.T) { - phoneType := phoneutil.TypeMobile - mcc := 302 - mnc := 720 - event := &phone.Event{ - Type: phone.EventTypeVerificationCodeSent, - VerificationId: "verification_id", - PhoneNumber: "+12223334444", - PhoneMetadata: &phoneutil.Metadata{ - PhoneNumber: "+12223334444", - Type: &phoneType, - MobileCountryCode: &mcc, - MobileNetworkCode: &mnc, - }, - CreatedAt: time.Now(), - } - - model, err := toEventModel(event) - require.NoError(t, err) - - actual := fromEventModel(model) - assert.EqualValues(t, event, actual) -} diff --git a/pkg/code/data/phone/postgres/store.go b/pkg/code/data/phone/postgres/store.go deleted file mode 100644 index 95947280..00000000 --- a/pkg/code/data/phone/postgres/store.go +++ /dev/null @@ -1,161 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/phone" -) - -type store struct { - db *sqlx.DB -} - -// New returns a postgres backed phone.Store. -func New(db *sql.DB) phone.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// SaveVerification implements phone.Store.SaveVerification -func (s *store) SaveVerification(ctx context.Context, v *phone.Verification) error { - model, err := toVerificationModel(v) - if err != nil { - return err - } - - return model.dbSave(ctx, s.db) -} - -// GetVerification implements phone.Store.GetVerification -func (s *store) GetVerification(ctx context.Context, account, phoneNumber string) (*phone.Verification, error) { - model, err := dbGetVerification(ctx, s.db, account, phoneNumber) - if err != nil { - return nil, err - } - - return fromVerificationModel(model), nil -} - -// GetLatestVerificationForAccount implements phone.Store.GetLatestVerificationForAccount -func (s *store) GetLatestVerificationForAccount(ctx context.Context, account string) (*phone.Verification, error) { - model, err := dbGetLatestVerificationForAccount(ctx, s.db, account) - if err != nil { - return nil, err - } - - return fromVerificationModel(model), nil -} - -// GetLatestVerificationForNumber implements phone.Store.GetLatestVerificationForNumber -func (s *store) GetLatestVerificationForNumber(ctx context.Context, phoneNumber string) (*phone.Verification, error) { - model, err := dbGetLatestVerificationForNumber(ctx, s.db, phoneNumber) - if err != nil { - return nil, err - } - - return fromVerificationModel(model), nil -} - -// GetAllVerificationsForNumber implements phone.Store.GetAllVerificationsForNumber -func (s *store) GetAllVerificationsForNumber(ctx context.Context, phoneNumber string) ([]*phone.Verification, error) { - models, err := dbGetAllVerificationsForNumber(ctx, s.db, phoneNumber) - if err != nil { - return nil, err - } - - verifications := make([]*phone.Verification, len(models)) - for i, model := range models { - verifications[i] = fromVerificationModel(model) - } - - return verifications, nil -} - -// SaveLinkingToken implements phone.Store.SaveLinkingToken -func (s *store) SaveLinkingToken(ctx context.Context, token *phone.LinkingToken) error { - model, err := toLinkingTokenModel(token) - if err != nil { - return err - } - - return model.dbSave(ctx, s.db) -} - -// UseLinkingToken implements phone.Store.UseLinkingToken -func (s *store) UseLinkingToken(ctx context.Context, phoneNumber, code string) error { - return dbUseLinkingToken(ctx, s.db, phoneNumber, code) -} - -// FilterVerifiedNumbers implements phone.Store.FilterVerifiedNumbers -func (s *store) FilterVerifiedNumbers(ctx context.Context, phoneNumbers []string) ([]string, error) { - return dbFilterVerifiedNumbers(ctx, s.db, phoneNumbers) -} - -// GetSettings implements phone.Store.GetSettings -func (s *store) GetSettings(ctx context.Context, phoneNumber string) (*phone.Settings, error) { - models, err := dbGetOwnerAccountSettings(ctx, s.db, phoneNumber) - if err != nil { - return nil, err - } - - settings := &phone.Settings{ - PhoneNumber: phoneNumber, - ByOwnerAccount: make(map[string]*phone.OwnerAccountSetting), - } - - for _, model := range models { - settings.ByOwnerAccount[model.OwnerAccount] = fromOwnerAccountSettingModel(model) - } - - return settings, nil -} - -// SaveOwnerAccountSetting implements phone.Store.SaveOwnerAccountSetting -func (s *store) SaveOwnerAccountSetting(ctx context.Context, phoneNumber string, newSettings *phone.OwnerAccountSetting) error { - model, err := toOwnerAccountSettingModel(phoneNumber, newSettings) - if err != nil { - return err - } - - return model.dbSave(ctx, s.db) -} - -// PutEvent implements phone.Store.PutEvent -func (s *store) PutEvent(ctx context.Context, event *phone.Event) error { - model, err := toEventModel(event) - if err != nil { - return err - } - - return model.dbSave(ctx, s.db) -} - -// GetLatestEventForNumberByType implements phone.Store.GetLatestEventForNumberByType -func (s *store) GetLatestEventForNumberByType(ctx context.Context, phoneNumber string, eventType phone.EventType) (*phone.Event, error) { - model, err := dbGetLatestEventForNumberByType(ctx, s.db, phoneNumber, eventType) - if err != nil { - return nil, err - } - - return fromEventModel(model), nil -} - -// CountEventsForVerificationByType implements phone.Store.CountEventsForVerificationByType -func (s *store) CountEventsForVerificationByType(ctx context.Context, verification string, eventType phone.EventType) (uint64, error) { - return dbCountEventsForVerificationByType(ctx, s.db, verification, eventType) -} - -// CountEventsForNumberByTypeSinceTimestamp implements phone.Store.CountEventsForNumberByTypeSinceTimestamp -func (s *store) CountEventsForNumberByTypeSinceTimestamp(ctx context.Context, phoneNumber string, eventType phone.EventType, since time.Time) (uint64, error) { - return dbCountEventsForNumberByTypeSinceTimestamp(ctx, s.db, phoneNumber, eventType, since) -} - -// CountUniqueVerificationIdsForNumberSinceTimestamp implements phone.Store.CountUniqueVerificationIdsForNumberSinceTimestamp -func (s *store) CountUniqueVerificationIdsForNumberSinceTimestamp(ctx context.Context, phoneNumber string, since time.Time) (uint64, error) { - return dbCountUniqueVerificationIdsForNumberSinceTimestamp(ctx, s.db, phoneNumber, since) -} diff --git a/pkg/code/data/phone/postgres/store_test.go b/pkg/code/data/phone/postgres/store_test.go deleted file mode 100644 index 25ef372f..00000000 --- a/pkg/code/data/phone/postgres/store_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/phone/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_phoneverification( - id SERIAL NOT NULL PRIMARY KEY, - - phone_number TEXT NOT NULL, - owner_account TEXT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - last_verified_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_phoneverification__uniq__owner_account__and__phone_number UNIQUE (owner_account, phone_number) - ); - - CREATE TABLE codewallet__core_phonelinkingtoken( - id SERIAL NOT NULL PRIMARY KEY, - - phone_number TEXT NOT NULL, - code TEXT NOT NULL, - current_check_count INTEGER NOT NULL, - max_check_count INTEGER NOT NULL, - expires_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_phonelinkingtoken__uniq__phone_number UNIQUE (phone_number) - ); - - CREATE TABLE codewallet__core_phonesetting( - id SERIAL NOT NULL PRIMARY KEY, - - phone_number TEXT NOT NULL, - owner_account TEXT NOT NULL, - is_unlinked BOOL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - last_updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_phonesetting__uniq__owner_account__and__phone_number UNIQUE (owner_account, phone_number) - ); - - CREATE TABLE codewallet__core_phoneevent( - id SERIAL NOT NULL PRIMARY KEY, - - event_type INTEGER NOT NULL, - - verification_id TEXT NOT NULL, - - phone_number TEXT NOT NULL, - phone_type INTEGER NULL, - mobile_country_code INTEGER NULL, - mobile_network_code INTEGER NULL, - - created_at TIMESTAMP WITH TIME ZONE NOT NULL - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_phoneverification; - DROP TABLE codewallet__core_phonelinkingtoken; - DROP TABLE codewallet__core_phonesetting; - DROP TABLE codewallet__core_phoneevent; - ` -) - -var ( - testStore phone.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestPhonePostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/phone/store.go b/pkg/code/data/phone/store.go deleted file mode 100644 index ce0de6f9..00000000 --- a/pkg/code/data/phone/store.go +++ /dev/null @@ -1,270 +0,0 @@ -package phone - -import ( - "context" - "errors" - "time" - - "github.com/code-payments/code-server/pkg/phone" -) - -var ( - // ErrVerificationNotFound is returned when no verification(s) are found. - ErrVerificationNotFound = errors.New("phone verification not found") - - // ErrInvalidVerification is returned if the verification is invalid. - ErrInvalidVerification = errors.New("verification is invalid") - - // ErrMetadataNotFound is returned when no metadata is found. - ErrMetadataNotFound = errors.New("phone metadata not found") - - // ErrLinkingTokenNotFound is returned when no link token is found. - ErrLinkingTokenNotFound = errors.New("linking token not found") - - // ErrEventNotFound is returned when no phone event is found. - ErrEventNotFound = errors.New("event not found") -) - -const ( - // Phone number used to migrate accounts that were used in Code prior to - // phone verification. - GrandFatheredPhoneNumber = "+16472222222" -) - -type EventType uint8 - -const ( - EventTypeUnknown EventType = iota - EventTypeVerificationCodeSent - EventTypeCheckVerificationCode - EventTypeVerificationCompleted -) - -type Verification struct { - PhoneNumber string - OwnerAccount string - CreatedAt time.Time - LastVerifiedAt time.Time -} - -type LinkingToken struct { - PhoneNumber string - Code string - CurrentCheckCount uint32 - MaxCheckCount uint32 - ExpiresAt time.Time -} - -type Settings struct { - PhoneNumber string - ByOwnerAccount map[string]*OwnerAccountSetting -} - -type OwnerAccountSetting struct { - OwnerAccount string - IsUnlinked *bool - CreatedAt time.Time - LastUpdatedAt time.Time -} - -type Event struct { - Type EventType - - VerificationId string - - PhoneNumber string - PhoneMetadata *phone.Metadata - - CreatedAt time.Time -} - -type Store interface { - // SaveVerification upserts a verification. Updates will only occur on newer - // verifications and won't lower existing permission levels. - SaveVerification(ctx context.Context, v *Verification) error - - // GetVerification gets a phone verification for the provided owner account - // and phone number pair. - GetVerification(ctx context.Context, account, phoneNumber string) (*Verification, error) - - // GetLatestVerificationForAccount gets the latest verification for a given - // owner account. - GetLatestVerificationForAccount(ctx context.Context, account string) (*Verification, error) - - // GetLatestVerificationForNumber gets the latest verification for a given - // phone number. - GetLatestVerificationForNumber(ctx context.Context, phoneNumber string) (*Verification, error) - - // GetAllVerificationsForNumber gets all phone verifications for a given - // phoneNumber. The returned value will be order by the last verification - // time. - // - // todo: May want to consider using cursors, but for now we expect the number - // of owner accounts per number to be relatively small. - GetAllVerificationsForNumber(ctx context.Context, phoneNumber string) ([]*Verification, error) - - // SaveLinkingToken uperts a phone linking token. - SaveLinkingToken(ctx context.Context, token *LinkingToken) error - - // UseLinkingToken enforces the one-time use of the token if the phone number - // and code pair matches. - // - // todo: Enforce one active code per phone number with a limited numer of checks, - // as a security measure. - UseLinkingToken(ctx context.Context, phoneNumber, code string) error - - // FilterVerifiedNumbers filters phone numbers that have been verified. - FilterVerifiedNumbers(ctx context.Context, phoneNumbers []string) ([]string, error) - - // GetSettings gets settings for a phone number. The implementation guarantee - // an empty setting is returned if the DB entry doesn't exist. - GetSettings(ctx context.Context, phoneNumber string) (*Settings, error) - - // SaveOwnerAccountSetting saves a phone setting for a given owner account. - // Only non-nil settings will be updated. - SaveOwnerAccountSetting(ctx context.Context, phoneNumber string, newSettings *OwnerAccountSetting) error - - // PutEvent stores a new phone event - PutEvent(ctx context.Context, event *Event) error - - // GetLatestEventForNumberByType gets the latest event for a phone number by type - GetLatestEventForNumberByType(ctx context.Context, phoneNumber string, eventType EventType) (*Event, error) - - // CountEventsForVerificationByType gets the count of events by type for a given verification - CountEventsForVerificationByType(ctx context.Context, verification string, eventType EventType) (uint64, error) - - // CountEventsForNumberByType gets the count of events by type for a given phone number since a - // timestamp - CountEventsForNumberByTypeSinceTimestamp(ctx context.Context, phoneNumber string, eventType EventType, since time.Time) (uint64, error) - - // CountUniqueVerificationIdsForNumberSinceTimestamp counts the number of unique verifications a - // phone number has been involved in since a timestamp. - CountUniqueVerificationIdsForNumberSinceTimestamp(ctx context.Context, phoneNumber string, since time.Time) (uint64, error) -} - -// Validate validates a Verification -func (v *Verification) Validate() error { - if v == nil { - return errors.New("verification is nil") - } - - if !phone.IsE164Format(v.PhoneNumber) { - return errors.New("phone number doesn't match E.164 standard") - } - - if len(v.OwnerAccount) == 0 { - return errors.New("owner account cannot be empty") - } - - if v.CreatedAt.IsZero() { - return errors.New("creation time is zero") - } - - if v.LastVerifiedAt.IsZero() { - return errors.New("verification time is zero") - } - - return nil -} - -func (t *LinkingToken) Validate() error { - if t == nil { - return errors.New("linking token is nil") - } - - if !phone.IsE164Format(t.PhoneNumber) { - return errors.New("phone number doesn't match E.164 standard") - } - - if !phone.IsVerificationCode(t.Code) { - return errors.New("verification code is not a 4-10 digit string") - } - - if t.ExpiresAt.IsZero() { - return errors.New("expiry time is zero") - } - - if t.ExpiresAt.Before(time.Now()) { - return errors.New("expirty time is in the past") - } - - if t.MaxCheckCount == 0 { - return errors.New("maximum check count must be positive") - } - - return nil -} - -func (s *Settings) Validate() error { - if s == nil { - return errors.New("phone settings is nil") - } - - if !phone.IsE164Format(s.PhoneNumber) { - return errors.New("phone number doesn't match E.164 standard") - } - - for ownerAccount, setting := range s.ByOwnerAccount { - if ownerAccount != setting.OwnerAccount { - return errors.New("invalid owner account setting map key") - } - - if err := setting.Validate(); err != nil { - return err - } - } - - return nil -} - -func (s *OwnerAccountSetting) Validate() error { - if s == nil { - return errors.New("owner account settings is nil") - } - - if len(s.OwnerAccount) == 0 { - return errors.New("owner account cannot be empty") - } - - if s.CreatedAt.IsZero() { - return errors.New("creation time is zero") - } - - if s.LastUpdatedAt.IsZero() { - return errors.New("last update time is zero") - } - - return nil -} - -func (e *Event) Validate() error { - if e == nil { - return errors.New("event is nil") - } - - if e.Type == EventTypeUnknown { - return errors.New("event type is required") - } - - if len(e.VerificationId) == 0 { - return errors.New("verification id is required") - } - - if !phone.IsE164Format(e.PhoneNumber) { - return errors.New("phone number doesn't match E.164 standard") - } - - if e.PhoneMetadata == nil { - return errors.New("phone metadata is required") - } - - if e.PhoneMetadata != nil && e.PhoneNumber != e.PhoneMetadata.PhoneNumber { - return errors.New("mismatched phone metadata detected") - } - - if e.CreatedAt.IsZero() { - return errors.New("creation time is zero") - } - - return nil -} diff --git a/pkg/code/data/phone/tests/tests.go b/pkg/code/data/phone/tests/tests.go deleted file mode 100644 index af20a2de..00000000 --- a/pkg/code/data/phone/tests/tests.go +++ /dev/null @@ -1,511 +0,0 @@ -package tests - -import ( - "context" - "crypto/ed25519" - "fmt" - "testing" - "time" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - phoneutil "github.com/code-payments/code-server/pkg/phone" - "github.com/code-payments/code-server/pkg/code/data/phone" -) - -func RunTests(t *testing.T, s phone.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s phone.Store){ - testVerificationHappyPath, - testGetVerification, - testGetLatestVerificationForAccount, - testGetLatestVerificationForNumber, - testUpdateVerification, - testGetAllVerificationsForNumber, - testLinkingTokenHappyPath, - testFilterVerifiedNumbers, - testSettingsHappyPath, - testEventHappyPath, - } { - tf(t, s) - teardown() - } -} - -func testVerificationHappyPath(t *testing.T, s phone.Store) { - t.Run("testVerificationHappyPath", func(t *testing.T) { - ctx := context.Background() - - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - verification := &phone.Verification{ - PhoneNumber: "+11234567890", - OwnerAccount: base58.Encode(pub), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - - _, err = s.GetVerification(ctx, verification.OwnerAccount, verification.PhoneNumber) - assert.Equal(t, phone.ErrVerificationNotFound, err) - - _, err = s.GetLatestVerificationForAccount(ctx, verification.OwnerAccount) - assert.Equal(t, phone.ErrVerificationNotFound, err) - - _, err = s.GetLatestVerificationForNumber(ctx, verification.OwnerAccount) - assert.Equal(t, phone.ErrVerificationNotFound, err) - - _, err = s.GetAllVerificationsForNumber(ctx, verification.PhoneNumber) - assert.Equal(t, phone.ErrVerificationNotFound, err) - - require.NoError(t, s.SaveVerification(ctx, verification)) - - actual, err := s.GetVerification(ctx, verification.OwnerAccount, verification.PhoneNumber) - require.NoError(t, err) - assertEqualVerifications(t, verification, actual) - - actual, err = s.GetLatestVerificationForAccount(ctx, verification.OwnerAccount) - require.NoError(t, err) - assertEqualVerifications(t, verification, actual) - - actual, err = s.GetLatestVerificationForNumber(ctx, verification.PhoneNumber) - require.NoError(t, err) - assertEqualVerifications(t, verification, actual) - - all, err := s.GetAllVerificationsForNumber(ctx, verification.PhoneNumber) - require.NoError(t, err) - require.Len(t, all, 1) - assertEqualVerifications(t, verification, all[0]) - }) -} - -func testGetVerification(t *testing.T, s phone.Store) { - t.Run("testGetVerification", func(t *testing.T) { - ctx := context.Background() - - for i := 0; i < 3; i++ { - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - verification := &phone.Verification{ - PhoneNumber: fmt.Sprintf("+1800555000%d", i), - OwnerAccount: base58.Encode(pub), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now().Add(time.Duration(i) * time.Hour), - } - - _, err = s.GetVerification(ctx, verification.OwnerAccount, verification.PhoneNumber) - assert.Equal(t, phone.ErrVerificationNotFound, err) - - require.NoError(t, s.SaveVerification(ctx, verification)) - - actual, err := s.GetVerification(ctx, verification.OwnerAccount, verification.PhoneNumber) - require.NoError(t, err) - assertEqualVerifications(t, verification, actual) - } - }) -} - -func testGetLatestVerificationForAccount(t *testing.T, s phone.Store) { - t.Run("testGetLatestVerificationForAccount", func(t *testing.T) { - ctx := context.Background() - - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - ownerAccount := base58.Encode(pub) - - var verifications []*phone.Verification - for i := 0; i < 3; i++ { - verification := &phone.Verification{ - PhoneNumber: fmt.Sprintf("+1800555000%d", i), - OwnerAccount: ownerAccount, - CreatedAt: time.Now(), - LastVerifiedAt: time.Now().Add(time.Duration(i) * time.Hour), - } - require.NoError(t, s.SaveVerification(ctx, verification)) - - verifications = append(verifications, verification) - } - - actual, err := s.GetLatestVerificationForAccount(ctx, ownerAccount) - require.NoError(t, err) - assertEqualVerifications(t, verifications[len(verifications)-1], actual) - }) -} - -func testGetLatestVerificationForNumber(t *testing.T, s phone.Store) { - t.Run("testGetLatestVerificationForNumber", func(t *testing.T) { - ctx := context.Background() - - var verifications []*phone.Verification - for i := 0; i < 3; i++ { - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - verification := &phone.Verification{ - PhoneNumber: "+11234567890", - OwnerAccount: base58.Encode(pub), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now().Add(time.Duration(i) * time.Hour), - } - require.NoError(t, s.SaveVerification(ctx, verification)) - - verifications = append(verifications, verification) - } - - actual, err := s.GetLatestVerificationForNumber(ctx, verifications[0].PhoneNumber) - require.NoError(t, err) - assertEqualVerifications(t, verifications[len(verifications)-1], actual) - }) -} - -func testUpdateVerification(t *testing.T, s phone.Store) { - t.Run("testUpdateVerification", func(t *testing.T) { - ctx := context.Background() - - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - verification := &phone.Verification{ - PhoneNumber: "+11234567890", - OwnerAccount: base58.Encode(pub), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - } - - require.NoError(t, s.SaveVerification(ctx, verification)) - - assert.Equal(t, phone.ErrInvalidVerification, s.SaveVerification(ctx, verification)) - - verification.CreatedAt = verification.CreatedAt.Add(1 * time.Hour) - verification.LastVerifiedAt = verification.LastVerifiedAt.Add(1 * time.Hour) - require.NoError(t, s.SaveVerification(ctx, verification)) - - actual, err := s.GetLatestVerificationForAccount(ctx, verification.OwnerAccount) - require.NoError(t, err) - verification.CreatedAt = verification.CreatedAt.Add(-1 * time.Hour) - assertEqualVerifications(t, verification, actual) - - verification.PhoneNumber = "+12223334444" - verification.LastVerifiedAt = verification.LastVerifiedAt.Add(1 * time.Hour) - require.NoError(t, s.SaveVerification(ctx, verification)) - - actual, err = s.GetLatestVerificationForAccount(ctx, verification.OwnerAccount) - require.NoError(t, err) - assertEqualVerifications(t, verification, actual) - }) -} - -func testGetAllVerificationsForNumber(t *testing.T, s phone.Store) { - t.Run("testGetAllVerificationsForNumber", func(t *testing.T) { - ctx := context.Background() - - var verifications []*phone.Verification - for i := 0; i < 3; i++ { - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - verification := &phone.Verification{ - PhoneNumber: "+11234567890", - OwnerAccount: base58.Encode(pub), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now().Add(time.Duration(i) * time.Hour), - } - require.NoError(t, s.SaveVerification(ctx, verification)) - - verifications = append(verifications, verification) - } - - all, err := s.GetAllVerificationsForNumber(ctx, verifications[0].PhoneNumber) - require.NoError(t, err) - require.Len(t, all, len(verifications)) - for i, actual := range all { - assertEqualVerifications(t, verifications[len(verifications)-i-1], actual) - } - }) -} - -func testLinkingTokenHappyPath(t *testing.T, s phone.Store) { - t.Run("testLinkingTokenHappyPath", func(t *testing.T) { - ctx := context.Background() - - token := &phone.LinkingToken{ - PhoneNumber: "+11234567890", - Code: "123456", - ExpiresAt: time.Now().Add(1 * time.Hour), - MaxCheckCount: 1, - } - - err := s.UseLinkingToken(ctx, token.PhoneNumber, token.Code) - assert.Equal(t, phone.ErrLinkingTokenNotFound, err) - - require.NoError(t, s.SaveLinkingToken(ctx, token)) - - require.NoError(t, s.UseLinkingToken(ctx, token.PhoneNumber, token.Code)) - err = s.UseLinkingToken(ctx, token.PhoneNumber, token.Code) - assert.Equal(t, phone.ErrLinkingTokenNotFound, err) - - originalCode := token.Code - require.NoError(t, s.SaveLinkingToken(ctx, token)) - token.Code = "7890" - token.MaxCheckCount = 2 - require.NoError(t, s.SaveLinkingToken(ctx, token)) - - err = s.UseLinkingToken(ctx, token.PhoneNumber, originalCode) - assert.Equal(t, phone.ErrLinkingTokenNotFound, err) - - require.NoError(t, s.UseLinkingToken(ctx, token.PhoneNumber, token.Code)) - err = s.UseLinkingToken(ctx, token.PhoneNumber, token.Code) - assert.Equal(t, phone.ErrLinkingTokenNotFound, err) - - require.NoError(t, s.SaveLinkingToken(ctx, token)) - for i := 0; i < int(token.MaxCheckCount); i++ { - err = s.UseLinkingToken(ctx, token.PhoneNumber, originalCode) - assert.Equal(t, phone.ErrLinkingTokenNotFound, err) - } - - err = s.UseLinkingToken(ctx, token.PhoneNumber, token.Code) - assert.Equal(t, phone.ErrLinkingTokenNotFound, err) - }) -} - -func testFilterVerifiedNumbers(t *testing.T, s phone.Store) { - t.Run("testFilterVerifiedNumbers", func(t *testing.T) { - ctx := context.Background() - - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - ownerAccount := base58.Encode(pub) - - var phoneNumbers []string - for i := 0; i < 10; i++ { - phoneNumbers = append(phoneNumbers, fmt.Sprintf("+1800555000%d", i)) - } - - filtered, err := s.FilterVerifiedNumbers(ctx, phoneNumbers) - require.NoError(t, err) - assert.Empty(t, filtered) - - for i, phoneNumber := range phoneNumbers[:len(phoneNumbers)/2] { - verification := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount, - CreatedAt: time.Now(), - LastVerifiedAt: time.Now().Add(time.Duration(i) * time.Hour), - } - require.NoError(t, s.SaveVerification(ctx, verification)) - } - - filtered, err = s.FilterVerifiedNumbers(ctx, phoneNumbers) - require.NoError(t, err) - assert.Equal(t, phoneNumbers[:len(phoneNumbers)/2], filtered) - }) -} - -func testSettingsHappyPath(t *testing.T, s phone.Store) { - t.Run("testSettingsHappyPath", func(t *testing.T) { - ctx := context.Background() - - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - ownerAccount := base58.Encode(pub) - - phoneNumber := "+11234567890" - - actual, err := s.GetSettings(ctx, phoneNumber) - require.NoError(t, err) - assert.Equal(t, phoneNumber, actual.PhoneNumber) - assert.Empty(t, actual.ByOwnerAccount) - - now := time.Now() - expectedSettings := &phone.OwnerAccountSetting{ - OwnerAccount: ownerAccount, - CreatedAt: now, - LastUpdatedAt: now, - } - - require.NoError(t, s.SaveOwnerAccountSetting(ctx, phoneNumber, expectedSettings)) - - actual, err = s.GetSettings(ctx, phoneNumber) - require.NoError(t, err) - assert.Equal(t, phoneNumber, actual.PhoneNumber) - assert.Len(t, actual.ByOwnerAccount, 1) - - actualForOwnerAccount, ok := actual.ByOwnerAccount[ownerAccount] - require.True(t, ok) - assertEqualOwnerAccountSettings(t, expectedSettings, actualForOwnerAccount) - - now = time.Now() - isUnlinked := true - expectedSettings.IsUnlinked = &isUnlinked - expectedSettings.CreatedAt = now - expectedSettings.LastUpdatedAt = now - require.NoError(t, s.SaveOwnerAccountSetting(ctx, phoneNumber, expectedSettings)) - - actual, err = s.GetSettings(ctx, phoneNumber) - require.NoError(t, err) - assert.Equal(t, phoneNumber, actual.PhoneNumber) - assert.Len(t, actual.ByOwnerAccount, 1) - - actualForOwnerAccount, ok = actual.ByOwnerAccount[ownerAccount] - require.True(t, ok) - assert.Equal(t, ownerAccount, actualForOwnerAccount.OwnerAccount) - require.NotNil(t, actualForOwnerAccount.IsUnlinked) - assert.True(t, *actualForOwnerAccount.IsUnlinked) - assert.True(t, actualForOwnerAccount.LastUpdatedAt.After(actualForOwnerAccount.CreatedAt)) - assert.Equal(t, expectedSettings.LastUpdatedAt.Unix(), actualForOwnerAccount.LastUpdatedAt.Unix()) - - isUnlinked = false - expectedSettings.LastUpdatedAt = time.Now() - require.NoError(t, s.SaveOwnerAccountSetting(ctx, phoneNumber, expectedSettings)) - - actual, err = s.GetSettings(ctx, phoneNumber) - require.NoError(t, err) - assert.Equal(t, phoneNumber, actual.PhoneNumber) - assert.Len(t, actual.ByOwnerAccount, 1) - - actualForOwnerAccount, ok = actual.ByOwnerAccount[ownerAccount] - require.True(t, ok) - require.NotNil(t, actualForOwnerAccount.IsUnlinked) - assert.False(t, *actualForOwnerAccount.IsUnlinked) - - expectedSettings.IsUnlinked = nil - expectedSettings.LastUpdatedAt = time.Now() - require.NoError(t, s.SaveOwnerAccountSetting(ctx, phoneNumber, expectedSettings)) - - actual, err = s.GetSettings(ctx, phoneNumber) - require.NoError(t, err) - assert.Equal(t, phoneNumber, actual.PhoneNumber) - assert.Len(t, actual.ByOwnerAccount, 1) - - actualForOwnerAccount, ok = actual.ByOwnerAccount[ownerAccount] - require.True(t, ok) - require.NotNil(t, actualForOwnerAccount.IsUnlinked) - assert.False(t, *actualForOwnerAccount.IsUnlinked) - }) -} - -func testEventHappyPath(t *testing.T, s phone.Store) { - t.Run("testEventHappyPath", func(t *testing.T) { - start := time.Now() - - ctx := context.Background() - - for i := 0; i < 5; i++ { - expected := &phone.Event{ - Type: phone.EventTypeVerificationCodeSent, - VerificationId: "verification_id", - PhoneNumber: "+12223334444", - PhoneMetadata: &phoneutil.Metadata{ - PhoneNumber: "+12223334444", - }, - CreatedAt: time.Now().Add(time.Duration(i) * time.Second), - } - - count, err := s.CountUniqueVerificationIdsForNumberSinceTimestamp(ctx, expected.PhoneNumber, start) - require.NoError(t, err) - if i == 0 { - assert.EqualValues(t, 0, count) - } else { - assert.EqualValues(t, 1, count) - } - - count, err = s.CountEventsForVerificationByType(ctx, expected.VerificationId, expected.Type) - require.NoError(t, err) - assert.EqualValues(t, i, count) - - count, err = s.CountEventsForNumberByTypeSinceTimestamp(ctx, expected.PhoneNumber, expected.Type, start) - require.NoError(t, err) - assert.EqualValues(t, i, count) - - require.NoError(t, s.PutEvent(ctx, expected)) - - actual, err := s.GetLatestEventForNumberByType(ctx, expected.PhoneNumber, expected.Type) - require.NoError(t, err) - assertEqualEvents(t, expected, actual) - } - - for i := 0; i < 5; i++ { - event := &phone.Event{ - Type: phone.EventTypeVerificationCodeSent, - VerificationId: fmt.Sprintf("verification%d", i), - PhoneNumber: "+12223334444", - PhoneMetadata: &phoneutil.Metadata{ - PhoneNumber: "+12223334444", - }, - CreatedAt: time.Now().Add(time.Duration(i) * time.Second), - } - - count, err := s.CountUniqueVerificationIdsForNumberSinceTimestamp(ctx, event.PhoneNumber, start) - require.NoError(t, err) - assert.EqualValues(t, i+1, count) - - require.NoError(t, s.PutEvent(ctx, event)) - } - - // Phone number doesn't have any events - _, err := s.GetLatestEventForNumberByType(ctx, "+18005550000", phone.EventTypeVerificationCodeSent) - assert.Equal(t, phone.ErrEventNotFound, err) - - // Phone number doesn't have any events of the provided type - _, err = s.GetLatestEventForNumberByType(ctx, "+12223334444", phone.EventTypeCheckVerificationCode) - assert.Equal(t, phone.ErrEventNotFound, err) - - // Verification doesn't exist - count, err := s.CountEventsForVerificationByType(ctx, "unknown_verification", phone.EventTypeVerificationCodeSent) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - // Phone number doesn't have any event of the provided type - count, err = s.CountEventsForNumberByTypeSinceTimestamp(ctx, "+18005550000", phone.EventTypeVerificationCodeSent, start) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - // Timestamp doesn't capture any verifications - count, err = s.CountEventsForNumberByTypeSinceTimestamp(ctx, "+12223334444", phone.EventTypeVerificationCodeSent, time.Now().Add(time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - // Number doesn't have any verifications - count, err = s.CountUniqueVerificationIdsForNumberSinceTimestamp(ctx, "+18005550000", start) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - - // Timestamp doesn't capture any verifications - count, err = s.CountUniqueVerificationIdsForNumberSinceTimestamp(ctx, "+12223334444", time.Now().Add(time.Hour)) - require.NoError(t, err) - assert.EqualValues(t, 0, count) - }) -} - -func assertEqualVerifications(t *testing.T, obj1, obj2 *phone.Verification) { - require.NoError(t, obj1.Validate()) - require.NoError(t, obj2.Validate()) - - assert.Equal(t, obj1.PhoneNumber, obj2.PhoneNumber) - assert.Equal(t, obj1.OwnerAccount, obj2.OwnerAccount) - assert.Equal(t, obj1.CreatedAt.Unix(), obj2.CreatedAt.Unix()) - assert.Equal(t, obj1.LastVerifiedAt.Unix(), obj2.LastVerifiedAt.Unix()) -} - -func assertEqualOwnerAccountSettings(t *testing.T, obj1, obj2 *phone.OwnerAccountSetting) { - assert.Equal(t, obj1.OwnerAccount, obj2.OwnerAccount) - assert.EqualValues(t, obj1.IsUnlinked, obj2.IsUnlinked) - assert.Equal(t, obj1.CreatedAt.Unix(), obj2.CreatedAt.Unix()) - assert.Equal(t, obj1.LastUpdatedAt.Unix(), obj2.LastUpdatedAt.Unix()) -} - -func assertEqualEvents(t *testing.T, obj1, obj2 *phone.Event) { - require.NoError(t, obj1.Validate()) - require.NoError(t, obj2.Validate()) - - assert.Equal(t, obj1.Type, obj2.Type) - assert.Equal(t, obj1.VerificationId, obj2.VerificationId) - assert.Equal(t, obj1.PhoneNumber, obj2.PhoneNumber) - assert.Equal(t, obj1.PhoneMetadata.PhoneNumber, obj2.PhoneMetadata.PhoneNumber) - assert.Equal(t, obj1.CreatedAt.Unix(), obj2.CreatedAt.Unix()) -} diff --git a/pkg/code/data/preferences/memory/store.go b/pkg/code/data/preferences/memory/store.go deleted file mode 100644 index 93cdb58e..00000000 --- a/pkg/code/data/preferences/memory/store.go +++ /dev/null @@ -1,92 +0,0 @@ -package memory - -import ( - "context" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -type store struct { - mu sync.Mutex - records []*preferences.Record - last uint64 -} - -// New returns a new in memory preferences.Store -func New() preferences.Store { - return &store{} -} - -// Save saves a preferences record -func (s *store) Save(_ context.Context, record *preferences.Record) error { - if err := record.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - - if item := s.find(record); item != nil { - record.LastUpdatedAt = time.Now() - - item.Locale = record.Locale - item.LastUpdatedAt = record.LastUpdatedAt - } else { - record.Id = s.last - record.LastUpdatedAt = time.Now() - - cloned := record.Clone() - s.records = append(s.records, &cloned) - } - - return nil -} - -// Get gets a a preference record by a data container -func (s *store) Get(_ context.Context, id *user.DataContainerID) (*preferences.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findByDataContainer(id) - if item == nil { - return nil, preferences.ErrPreferencesNotFound - } - - cloned := item.Clone() - return &cloned, nil -} - -func (s *store) find(data *preferences.Record) *preferences.Record { - for _, item := range s.records { - if item.Id == data.Id { - return item - } - - if item.DataContainerId.String() == data.DataContainerId.String() { - return item - } - } - return nil -} - -func (s *store) findByDataContainer(id *user.DataContainerID) *preferences.Record { - for _, item := range s.records { - if item.DataContainerId.String() == id.String() { - return item - } - } - return nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.last = 0 - s.records = nil -} diff --git a/pkg/code/data/preferences/memory/store_test.go b/pkg/code/data/preferences/memory/store_test.go deleted file mode 100644 index 55a0d24a..00000000 --- a/pkg/code/data/preferences/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/preferences/tests" -) - -func TestPreferencesMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/preferences/postgres/model.go b/pkg/code/data/preferences/postgres/model.go deleted file mode 100644 index 91082aa9..00000000 --- a/pkg/code/data/preferences/postgres/model.go +++ /dev/null @@ -1,100 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - "golang.org/x/text/language" - - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/user" - pgutil "github.com/code-payments/code-server/pkg/database/postgres" -) - -const ( - tableName = "codewallet__core_appuserpreferences" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - DataContainerId string `db:"data_container_id"` - Locale string `db:"locale"` - LastUpdatedAt time.Time `db:"last_updated_at"` -} - -func toModel(obj *preferences.Record) (*model, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &model{ - DataContainerId: obj.DataContainerId.String(), - Locale: obj.Locale.String(), - LastUpdatedAt: obj.LastUpdatedAt, - }, nil -} - -func fromModel(obj *model) (*preferences.Record, error) { - dataContainerID, err := user.GetDataContainerIDFromString(obj.DataContainerId) - if err != nil { - return nil, err - } - - locale, err := language.Parse(obj.Locale) - if err != nil { - return nil, err - } - - return &preferences.Record{ - Id: uint64(obj.Id.Int64), - DataContainerId: *dataContainerID, - Locale: locale, - LastUpdatedAt: obj.LastUpdatedAt, - }, nil -} - -func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + tableName + ` - (data_container_id, locale, last_updated_at) - VALUES ($1, $2, $3) - - ON CONFLICT (data_container_id) - DO UPDATE - SET locale = $2, last_updated_at = $3 - WHERE ` + tableName + `.data_container_id = $1 - - - RETURNING id, data_container_id, locale, last_updated_at - ` - - m.LastUpdatedAt = time.Now() - - return db.QueryRowxContext( - ctx, - query, - m.DataContainerId, - m.Locale, - m.LastUpdatedAt.UTC(), - ).StructScan(m) -} - -func dbGet(ctx context.Context, db *sqlx.DB, id *user.DataContainerID) (*model, error) { - res := &model{} - - query := `SELECT id, data_container_id, locale, last_updated_at FROM ` + tableName + ` - WHERE data_container_id = $1` - - err := db.GetContext( - ctx, - res, - query, - id.String(), - ) - if err != nil { - return nil, pgutil.CheckNoRows(err, preferences.ErrPreferencesNotFound) - } - - return res, nil -} diff --git a/pkg/code/data/preferences/postgres/store.go b/pkg/code/data/preferences/postgres/store.go deleted file mode 100644 index 7cf706e1..00000000 --- a/pkg/code/data/preferences/postgres/store.go +++ /dev/null @@ -1,53 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new in postgres preferences.Store -func New(db *sql.DB) preferences.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Save saves a preferences record -func (s *store) Save(ctx context.Context, record *preferences.Record) error { - m, err := toModel(record) - if err != nil { - return err - } - - err = m.dbSave(ctx, s.db) - if err != nil { - return err - } - - res, err := fromModel(m) - if err != nil { - return err - } - res.CopyTo(record) - - return nil -} - -// Get gets a a preference record by a data container -func (s *store) Get(ctx context.Context, id *user.DataContainerID) (*preferences.Record, error) { - m, err := dbGet(ctx, s.db, id) - if err != nil { - return nil, err - } - - return fromModel(m) -} diff --git a/pkg/code/data/preferences/postgres/store_test.go b/pkg/code/data/preferences/postgres/store_test.go deleted file mode 100644 index 22015d95..00000000 --- a/pkg/code/data/preferences/postgres/store_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/preferences/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -var ( - testStore preferences.Store - teardown func() -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_appuserpreferences ( - id SERIAL NOT NULL PRIMARY KEY, - - data_container_id UUID NOT NULL, - - locale TEXT NOT NULL, - - last_updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_appuserpreferences__uniq__data_container_id UNIQUE (data_container_id) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_appuserpreferences; - ` -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestPreferencesPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/preferences/preferences.go b/pkg/code/data/preferences/preferences.go deleted file mode 100644 index 0b04a503..00000000 --- a/pkg/code/data/preferences/preferences.go +++ /dev/null @@ -1,66 +0,0 @@ -package preferences - -import ( - "time" - - "github.com/pkg/errors" - "golang.org/x/text/language" - - "github.com/code-payments/code-server/pkg/code/data/user" -) - -var ( - defaultLocale = language.English -) - -type Record struct { - Id uint64 - - DataContainerId user.DataContainerID - - Locale language.Tag - - LastUpdatedAt time.Time -} - -func (r *Record) Validate() error { - if err := r.DataContainerId.Validate(); err != nil { - return errors.Wrap(err, "invalid data container id") - } - - if r.Locale.String() == language.Und.String() { - return errors.New("locale is undefined") - } - - return nil -} - -func (r *Record) Clone() Record { - return Record{ - Id: r.Id, - DataContainerId: r.DataContainerId, - Locale: r.Locale, - LastUpdatedAt: r.LastUpdatedAt, - } -} - -func (r *Record) CopyTo(dst *Record) { - dst.Id = r.Id - dst.DataContainerId = r.DataContainerId - dst.Locale = r.Locale - dst.LastUpdatedAt = r.LastUpdatedAt -} - -// GetDefaultPreferences returns the default set of user preferences -func GetDefaultPreferences(id *user.DataContainerID) *Record { - return &Record{ - DataContainerId: *id, - Locale: defaultLocale, - LastUpdatedAt: time.Now(), - } -} - -// GetDefaultLocale returns the default locale setting -func GetDefaultLocale() language.Tag { - return defaultLocale -} diff --git a/pkg/code/data/preferences/store.go b/pkg/code/data/preferences/store.go deleted file mode 100644 index f0ccac15..00000000 --- a/pkg/code/data/preferences/store.go +++ /dev/null @@ -1,21 +0,0 @@ -package preferences - -import ( - "context" - "errors" - - "github.com/code-payments/code-server/pkg/code/data/user" -) - -var ( - ErrPreferencesNotFound = errors.New("preferences record not found") -) - -type Store interface { - // Save saves a preferences record - Save(ctx context.Context, record *Record) error - - // Get gets a a preference record by a data container. ErrPreferencesNotFound - // is returned if no record exists. - Get(ctx context.Context, id *user.DataContainerID) (*Record, error) -} diff --git a/pkg/code/data/preferences/tests/tests.go b/pkg/code/data/preferences/tests/tests.go deleted file mode 100644 index 21a807d7..00000000 --- a/pkg/code/data/preferences/tests/tests.go +++ /dev/null @@ -1,67 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/text/language" - - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -func RunTests(t *testing.T, s preferences.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s preferences.Store){ - testRoundTrip, - } { - tf(t, s) - teardown() - } -} - -func testRoundTrip(t *testing.T, s preferences.Store) { - t.Run("testRoundTrip", func(t *testing.T) { - ctx := context.Background() - - containerId := user.NewDataContainerID() - - _, err := s.Get(ctx, containerId) - assert.Equal(t, preferences.ErrPreferencesNotFound, err) - - expected := preferences.GetDefaultPreferences(containerId) - cloned := expected.Clone() - - start := time.Now() - time.Sleep(time.Millisecond) - - require.NoError(t, s.Save(ctx, expected)) - assert.True(t, expected.Id > 0) - assert.True(t, expected.LastUpdatedAt.After(start)) - - actual, err := s.Get(ctx, containerId) - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - - expected.Locale = language.CanadianFrench - cloned = expected.Clone() - - start = time.Now() - time.Sleep(time.Millisecond) - - require.NoError(t, s.Save(ctx, expected)) - assert.Equal(t, cloned.Id, expected.Id) - assert.True(t, expected.LastUpdatedAt.After(start)) - - actual, err = s.Get(ctx, containerId) - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - }) -} - -func assertEquivalentRecords(t *testing.T, obj1, obj2 *preferences.Record) { - assert.Equal(t, obj1.DataContainerId, obj2.DataContainerId) - assert.Equal(t, obj1.Locale.String(), obj2.Locale.String()) -} diff --git a/pkg/code/data/push/memory/store.go b/pkg/code/data/push/memory/store.go deleted file mode 100644 index faf81a65..00000000 --- a/pkg/code/data/push/memory/store.go +++ /dev/null @@ -1,138 +0,0 @@ -package memory - -import ( - "context" - "sync" - - "github.com/code-payments/code-server/pkg/code/data/push" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -type store struct { - mu sync.Mutex - records []*push.Record - last uint64 -} - -// New returns a new in memory push.Store -func New() push.Store { - return &store{} -} - -// Put implements push.Store.Put -func (s *store) Put(_ context.Context, record *push.Record) error { - if err := record.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - - if item := s.find(record); item != nil { - return push.ErrTokenExists - } else { - record.Id = s.last - s.records = append(s.records, record.Clone()) - } - - return nil -} - -// MarkAsInvalid implements push.Store.MarkAsInvalid -func (s *store) MarkAsInvalid(ctx context.Context, pushToken string) error { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findByPushToken(pushToken) - for _, item := range items { - item.IsValid = false - } - - return nil -} - -// Delete implements push.Store.Delete -func (s *store) Delete(ctx context.Context, pushToken string) error { - s.mu.Lock() - defer s.mu.Unlock() - - var updated []*push.Record - for _, item := range s.records { - if item.PushToken != pushToken { - updated = append(updated, item) - } - } - s.records = updated - - return nil -} - -// GetAllValidByDataContainer implements push.Store.GetAllValidByDataContainer -func (s *store) GetAllValidByDataContainer(_ context.Context, id *user.DataContainerID) ([]*push.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - res := s.findByDataContainer(id) - res = s.filterInvalid(res) - - if len(res) == 0 { - return nil, push.ErrTokenNotFound - } - return res, nil -} - -func (s *store) find(data *push.Record) *push.Record { - for _, item := range s.records { - if item.Id == data.Id { - return item - } - if item.DataContainerId.String() == data.DataContainerId.String() && item.PushToken == data.PushToken { - return item - } - } - return nil -} - -func (s *store) findByPushToken(pushToken string) []*push.Record { - var res []*push.Record - for _, item := range s.records { - if item.PushToken != pushToken { - continue - } - - res = append(res, item) - } - return res -} - -func (s *store) findByDataContainer(id *user.DataContainerID) []*push.Record { - var res []*push.Record - for _, item := range s.records { - if item.DataContainerId.String() != id.String() { - continue - } - - res = append(res, item) - } - return res -} - -func (s *store) filterInvalid(items []*push.Record) []*push.Record { - var res []*push.Record - for _, item := range items { - if item.IsValid { - res = append(res, item) - } - } - return res -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.records = nil - s.last = 0 -} diff --git a/pkg/code/data/push/memory/store_test.go b/pkg/code/data/push/memory/store_test.go deleted file mode 100644 index 29b7cea2..00000000 --- a/pkg/code/data/push/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/push/tests" -) - -func TestPushMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/push/postgres/model.go b/pkg/code/data/push/postgres/model.go deleted file mode 100644 index 9da107ae..00000000 --- a/pkg/code/data/push/postgres/model.go +++ /dev/null @@ -1,121 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/push" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -const ( - tableName = "codewallet__core_pushtoken" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - DataContainerId string `db:"data_container_id"` - PushToken string `db:"push_token"` - TokenType uint `db:"token_type"` - IsValid bool `db:"is_valid"` - AppInstallId string `db:"app_install_id"` // Cannot be nullable, since it's a part of a unique constraint - CreatedAt time.Time `db:"created_at"` -} - -func toModel(obj *push.Record) (*model, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &model{ - DataContainerId: obj.DataContainerId.String(), - PushToken: obj.PushToken, - TokenType: uint(obj.TokenType), - IsValid: obj.IsValid, - AppInstallId: *pointer.StringOrDefault(obj.AppInstallId, ""), - CreatedAt: obj.CreatedAt, - }, nil -} - -func fromModel(obj *model) (*push.Record, error) { - dataContainerID, err := user.GetDataContainerIDFromString(obj.DataContainerId) - if err != nil { - return nil, err - } - - return &push.Record{ - Id: uint64(obj.Id.Int64), - DataContainerId: *dataContainerID, - PushToken: obj.PushToken, - TokenType: push.TokenType(obj.TokenType), - IsValid: obj.IsValid, - AppInstallId: pointer.StringIfValid(len(obj.AppInstallId) > 0, obj.AppInstallId), - CreatedAt: obj.CreatedAt, - }, nil -} - -func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + tableName + ` - (data_container_id, push_token, token_type, is_valid, app_install_id, created_at) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id, data_container_id, push_token, token_type, is_valid, app_install_id, created_at - ` - - err := db.QueryRowxContext( - ctx, - query, - m.DataContainerId, - m.PushToken, - m.TokenType, - m.IsValid, - m.AppInstallId, - m.CreatedAt, - ).StructScan(m) - - return pgutil.CheckUniqueViolation(err, push.ErrTokenExists) -} - -func dbMarkAsInvalid(ctx context.Context, db *sqlx.DB, pushToken string) error { - query := `UPDATE ` + tableName + ` - SET is_valid = false - WHERE push_token = $1 - ` - - _, err := db.ExecContext(ctx, query, pushToken) - return err -} - -func dbDelete(ctx context.Context, db *sqlx.DB, pushToken string) error { - query := `DELETE FROM ` + tableName + ` - WHERE push_token = $1 - ` - - _, err := db.ExecContext(ctx, query, pushToken) - return err -} - -func dbGetAllValidByDataContainer(ctx context.Context, db *sqlx.DB, id *user.DataContainerID) ([]*model, error) { - res := []*model{} - - query := `SELECT - id, data_container_id, push_token, token_type, is_valid, app_install_id, created_at - FROM ` + tableName + ` - WHERE data_container_id = $1 AND is_valid = true - ` - - err := db.SelectContext(ctx, &res, query, id.String()) - if err != nil { - return nil, pgutil.CheckNoRows(err, push.ErrTokenNotFound) - } - - if len(res) == 0 { - return nil, push.ErrTokenNotFound - } - - return res, nil -} diff --git a/pkg/code/data/push/postgres/store.go b/pkg/code/data/push/postgres/store.go deleted file mode 100644 index 923a3268..00000000 --- a/pkg/code/data/push/postgres/store.go +++ /dev/null @@ -1,70 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/push" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres-backed push.Store -func New(db *sql.DB) push.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Put implements push.Store.Put -func (s *store) Put(ctx context.Context, record *push.Record) error { - model, err := toModel(record) - if err != nil { - return err - } - - err = model.dbSave(ctx, s.db) - if err != nil { - return err - } - - res, err := fromModel(model) - if err != nil { - return err - } - res.CopyTo(record) - - return nil -} - -// MarkAsInvalid implements push.Store.MarkAsInvalid -func (s *store) MarkAsInvalid(ctx context.Context, pushToken string) error { - return dbMarkAsInvalid(ctx, s.db, pushToken) -} - -// Delete implements push.Store.Delete -func (s *store) Delete(ctx context.Context, pushToken string) error { - return dbDelete(ctx, s.db, pushToken) -} - -// GetAllValidByDataContainer implements push.Store.GetAllValidByDataContainer -func (s *store) GetAllValidByDataContainer(ctx context.Context, id *user.DataContainerID) ([]*push.Record, error) { - models, err := dbGetAllValidByDataContainer(ctx, s.db, id) - if err != nil { - return nil, err - } - - res := make([]*push.Record, len(models)) - for i, model := range models { - res[i], err = fromModel(model) - if err != nil { - return nil, err - } - } - return res, nil -} diff --git a/pkg/code/data/push/postgres/store_test.go b/pkg/code/data/push/postgres/store_test.go deleted file mode 100644 index f46a2401..00000000 --- a/pkg/code/data/push/postgres/store_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/push" - "github.com/code-payments/code-server/pkg/code/data/push/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_pushtoken( - id SERIAL NOT NULL PRIMARY KEY, - - data_container_id UUID NOT NULL, - - push_token TEXT NOT NULL, - token_type INTEGER NOT NULL, - is_valid BOOL NOT NULL, - - app_install_id TEXT NOT NULL, - - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_pushtoken__uniq__data_container_id__and__app_install_id__and__push_token UNIQUE (data_container_id, app_install_id, push_token) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_pushtoken; - ` -) - -var ( - testStore push.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestPushPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/push/push_token.go b/pkg/code/data/push/push_token.go deleted file mode 100644 index 21b8c58a..00000000 --- a/pkg/code/data/push/push_token.go +++ /dev/null @@ -1,78 +0,0 @@ -package push - -import ( - "time" - - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -type TokenType uint8 - -const ( - TokenTypeUnknown TokenType = iota - TokenTypeFcmAndroid - TokenTypeFcmApns -) - -type Record struct { - Id uint64 - - DataContainerId user.DataContainerID - - PushToken string - TokenType TokenType - IsValid bool - - AppInstallId *string - - CreatedAt time.Time -} - -func (r *Record) Clone() *Record { - return &Record{ - Id: r.Id, - DataContainerId: r.DataContainerId, - PushToken: r.PushToken, - TokenType: r.TokenType, - IsValid: r.IsValid, - AppInstallId: pointer.StringCopy(r.AppInstallId), - CreatedAt: r.CreatedAt, - } -} - -func (r *Record) CopyTo(dst *Record) { - dst.Id = r.Id - dst.DataContainerId = r.DataContainerId - dst.PushToken = r.PushToken - dst.TokenType = r.TokenType - dst.IsValid = r.IsValid - dst.AppInstallId = pointer.StringCopy(r.AppInstallId) - dst.CreatedAt = r.CreatedAt -} - -func (r *Record) Validate() error { - if err := r.DataContainerId.Validate(); err != nil { - return errors.Wrap(err, "invalid data container id") - } - - if len(r.PushToken) == 0 { - return errors.New("push token is required") - } - - if r.TokenType != TokenTypeFcmAndroid && r.TokenType != TokenTypeFcmApns { - return errors.New("invalid token type") - } - - if r.AppInstallId != nil && len(*r.AppInstallId) == 0 { - return errors.New("app install id is required when set") - } - - if r.CreatedAt.IsZero() { - return errors.New("creation timestamp is required") - } - - return nil -} diff --git a/pkg/code/data/push/store.go b/pkg/code/data/push/store.go deleted file mode 100644 index d574a874..00000000 --- a/pkg/code/data/push/store.go +++ /dev/null @@ -1,28 +0,0 @@ -package push - -import ( - "context" - "errors" - - "github.com/code-payments/code-server/pkg/code/data/user" -) - -var ( - ErrTokenExists = errors.New("push token already exists") - ErrTokenNotFound = errors.New("push token not found for data container") -) - -type Store interface { - // Put creates a new push token record - Put(ctx context.Context, record *Record) error - - // MarkAsInvalid marks all entries with the push token as invalid - MarkAsInvalid(ctx context.Context, pushToken string) error - - // Delete deletes all entries with the push token - Delete(ctx context.Context, pushToken string) error - - // GetAllValidByDataContainer gets all valid push token records for a given - // data container. - GetAllValidByDataContainer(ctx context.Context, id *user.DataContainerID) ([]*Record, error) -} diff --git a/pkg/code/data/push/tests/tests.go b/pkg/code/data/push/tests/tests.go deleted file mode 100644 index 4eea7b22..00000000 --- a/pkg/code/data/push/tests/tests.go +++ /dev/null @@ -1,177 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/push" - "github.com/code-payments/code-server/pkg/code/data/user" -) - -func RunTests(t *testing.T, s push.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s push.Store){ - testHappyPath, - testMarkAsInvalid, - testDelete, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s push.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - dataContainer := user.NewDataContainerID() - - _, err := s.GetAllValidByDataContainer(ctx, dataContainer) - assert.Equal(t, push.ErrTokenNotFound, err) - - var expected []*push.Record - for i := 0; i < 5; i++ { - tokenType := push.TokenTypeFcmAndroid - appInstallId := pointer.String("test_app_install") - if i%2 == 0 { - tokenType = push.TokenTypeFcmApns - appInstallId = nil - } - - record := &push.Record{ - DataContainerId: *dataContainer, - - PushToken: fmt.Sprintf("test_token_%d", i), - TokenType: tokenType, - IsValid: true, - - AppInstallId: appInstallId, - - CreatedAt: time.Now(), - } - require.NoError(t, s.Put(ctx, record)) - - expected = append(expected, record) - } - - actual, err := s.GetAllValidByDataContainer(ctx, dataContainer) - require.NoError(t, err) - - require.Len(t, actual, len(expected)) - - for i := 0; i < len(expected); i++ { - assert.EqualValues(t, i+1, actual[i].Id) - assert.EqualValues(t, i+1, expected[i].Id) - - assert.Equal(t, expected[i].DataContainerId.String(), actual[i].DataContainerId.String()) - - assert.Equal(t, expected[i].PushToken, actual[i].PushToken) - assert.Equal(t, expected[i].TokenType, actual[i].TokenType) - assert.True(t, actual[i].IsValid) - - assert.EqualValues(t, expected[i].AppInstallId, actual[i].AppInstallId) - - assert.Equal(t, expected[i].CreatedAt.Unix(), actual[i].CreatedAt.Unix()) - } - - for _, record := range expected { - err := s.Put(ctx, record) - assert.Equal(t, push.ErrTokenExists, err) - } - }) -} - -func testMarkAsInvalid(t *testing.T, s push.Store) { - t.Run("testMarkAsInvalid", func(t *testing.T) { - ctx := context.Background() - - var dataContainers []*user.DataContainerID - for i := 0; i < 5; i++ { - dataContainers = append(dataContainers, user.NewDataContainerID()) - } - - err := s.MarkAsInvalid(ctx, "invalid_token") - require.NoError(t, err) - - for _, dataContainer := range dataContainers { - for _, token := range []string{"valid_token", "invalid_token"} { - record := &push.Record{ - DataContainerId: *dataContainer, - - PushToken: token, - TokenType: push.TokenTypeFcmAndroid, - IsValid: true, - - CreatedAt: time.Now(), - } - require.NoError(t, s.Put(ctx, record)) - } - } - - for _, dataContainer := range dataContainers { - actual, err := s.GetAllValidByDataContainer(ctx, dataContainer) - require.NoError(t, err) - assert.Len(t, actual, 2) - } - - err = s.MarkAsInvalid(ctx, "invalid_token") - require.NoError(t, err) - - for _, dataContainer := range dataContainers { - actual, err := s.GetAllValidByDataContainer(ctx, dataContainer) - require.NoError(t, err) - require.Len(t, actual, 1) - assert.Equal(t, "valid_token", actual[0].PushToken) - } - }) -} - -func testDelete(t *testing.T, s push.Store) { - t.Run("testDelete", func(t *testing.T) { - ctx := context.Background() - - var dataContainers []*user.DataContainerID - for i := 0; i < 5; i++ { - dataContainers = append(dataContainers, user.NewDataContainerID()) - } - - err := s.Delete(ctx, "push_token_1") - require.NoError(t, err) - - for _, dataContainer := range dataContainers { - for _, token := range []string{"push_token_1", "push_token_2"} { - record := &push.Record{ - DataContainerId: *dataContainer, - - PushToken: token, - TokenType: push.TokenTypeFcmAndroid, - IsValid: true, - - CreatedAt: time.Now(), - } - require.NoError(t, s.Put(ctx, record)) - } - } - - for _, dataContainer := range dataContainers { - actual, err := s.GetAllValidByDataContainer(ctx, dataContainer) - require.NoError(t, err) - assert.Len(t, actual, 2) - } - - err = s.Delete(ctx, "push_token_1") - require.NoError(t, err) - - for _, dataContainer := range dataContainers { - actual, err := s.GetAllValidByDataContainer(ctx, dataContainer) - require.NoError(t, err) - require.Len(t, actual, 1) - assert.Equal(t, "push_token_2", actual[0].PushToken) - } - }) -} diff --git a/pkg/code/data/transaction/transaction.go b/pkg/code/data/transaction/transaction.go index 7ae9ceff..3ac88220 100644 --- a/pkg/code/data/transaction/transaction.go +++ b/pkg/code/data/transaction/transaction.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/code-payments/code-server/pkg/kin" + "github.com/code-payments/code-server/pkg/code/config" "github.com/code-payments/code-server/pkg/solana" "github.com/mr-tron/base58/base58" ) @@ -203,7 +203,7 @@ func getTokenBalanceSet(meta *solana.TransactionMeta, accounts []ed25519.PublicK txBalances := map[string]*TokenBalance{} for _, txTokenBal := range meta.PreTokenBalances { - if txTokenBal.Mint != kin.Mint { + if txTokenBal.Mint != config.CoreMintPublicKeyString { continue } @@ -225,7 +225,7 @@ func getTokenBalanceSet(meta *solana.TransactionMeta, accounts []ed25519.PublicK } for _, txTokenBal := range meta.PostTokenBalances { - if txTokenBal.Mint != kin.Mint { + if txTokenBal.Mint != config.CoreMintPublicKeyString { continue } diff --git a/pkg/code/data/treasury/memory/store.go b/pkg/code/data/treasury/memory/store.go deleted file mode 100644 index bb8d16bb..00000000 --- a/pkg/code/data/treasury/memory/store.go +++ /dev/null @@ -1,278 +0,0 @@ -package memory - -import ( - "context" - "sort" - "sync" - "time" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/treasury" -) - -type ById []*treasury.Record - -func (a ById) Len() int { return len(a) } -func (a ById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ById) Less(i, j int) bool { return a[i].Id < a[j].Id } - -type store struct { - mu sync.Mutex - treasuryPoolRecords []*treasury.Record - fundingRecords []*treasury.FundingHistoryRecord - last uint64 -} - -// New returns a new in memory treasury.Store -func New() treasury.Store { - return &store{} -} - -// Save implements treasury.Store.Save -func (s *store) Save(_ context.Context, data *treasury.Record) error { - if err := data.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - if item := s.findTreasuryPool(data); item != nil { - if data.SolanaBlock <= item.SolanaBlock { - return treasury.ErrStaleTreasuryPoolState - } - - historyList := make([]string, len(item.HistoryList)) - copy(historyList, data.HistoryList) - - item.SolanaBlock = data.SolanaBlock - item.CurrentIndex = data.CurrentIndex - item.HistoryList = historyList - - item.LastUpdatedAt = time.Now() - - item.CopyTo(data) - } else { - if data.Id == 0 { - data.Id = s.last - } - data.LastUpdatedAt = time.Now() - c := data.Clone() - s.treasuryPoolRecords = append(s.treasuryPoolRecords, c) - } - - return nil -} - -// GetByName implements treasury.Store.GetByName -func (s *store) GetByName(ctx context.Context, name string) (*treasury.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if item := s.findTreasuryPoolByName(name); item != nil { - return item.Clone(), nil - } - return nil, treasury.ErrTreasuryPoolNotFound -} - -// GetByAddress implements treasury.Store.GetByAddress -func (s *store) GetByAddress(_ context.Context, address string) (*treasury.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if item := s.findTreasuryPoolByAddress(address); item != nil { - return item.Clone(), nil - } - return nil, treasury.ErrTreasuryPoolNotFound -} - -// GetByVault implements treasury.Store.GetByVault -func (s *store) GetByVault(_ context.Context, vault string) (*treasury.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if item := s.findTreasuryPoolByVault(vault); item != nil { - return item.Clone(), nil - } - return nil, treasury.ErrTreasuryPoolNotFound -} - -// GetAllByState implements treasury.Store.GetAllByState -func (s *store) GetAllByState(_ context.Context, state treasury.TreasuryPoolState, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*treasury.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - if items := s.findTreasuryPoolByState(state); len(items) > 0 { - res := s.filterTreasuryPool(items, cursor, limit, direction) - - if len(res) == 0 { - return nil, treasury.ErrTreasuryPoolNotFound - } - - return res, nil - } - - return nil, treasury.ErrTreasuryPoolNotFound -} - -// SaveFunding implements treasury.Store.SaveFunding -func (s *store) SaveFunding(_ context.Context, data *treasury.FundingHistoryRecord) error { - s.mu.Lock() - defer s.mu.Unlock() - - if err := data.Validate(); err != nil { - return err - } - - s.last++ - if item := s.findFunding(data); item != nil { - item.State = data.State - - item.CopyTo(data) - } else { - if data.Id == 0 { - data.Id = s.last - } - c := data.Clone() - s.fundingRecords = append(s.fundingRecords, c) - } - - return nil -} - -// GetTotalAvailableFunds implements treasury.Store.GetTotalAvailableFunds -func (s *store) GetTotalAvailableFunds(_ context.Context, vault string) (uint64, error) { - s.mu.Lock() - defer s.mu.Unlock() - - var res int64 - items := s.findFundingByVault(vault) - for _, item := range items { - if item.DeltaQuarks > 0 && item.State == treasury.FundingStateConfirmed { - res += item.DeltaQuarks - } - - if item.DeltaQuarks < 0 && item.State != treasury.FundingStateFailed { - res += item.DeltaQuarks - } - } - - if res < 0 { - return 0, treasury.ErrNegativeFunding - } - return uint64(res), nil -} - -func (s *store) findTreasuryPool(data *treasury.Record) *treasury.Record { - for _, item := range s.treasuryPoolRecords { - if item.Id == data.Id { - return item - } - if data.Address == item.Address { - return item - } - } - return nil -} - -func (s *store) findTreasuryPoolByName(name string) *treasury.Record { - for _, item := range s.treasuryPoolRecords { - if name == item.Name { - return item - } - } - return nil -} - -func (s *store) findTreasuryPoolByAddress(address string) *treasury.Record { - for _, item := range s.treasuryPoolRecords { - if address == item.Address { - return item - } - } - return nil -} - -func (s *store) findTreasuryPoolByVault(vault string) *treasury.Record { - for _, item := range s.treasuryPoolRecords { - if vault == item.Vault { - return item - } - } - return nil -} - -func (s *store) findTreasuryPoolByState(state treasury.TreasuryPoolState) []*treasury.Record { - res := make([]*treasury.Record, 0) - for _, item := range s.treasuryPoolRecords { - if item.State == state { - res = append(res, item) - continue - } - } - return res -} - -func (s *store) filterTreasuryPool(items []*treasury.Record, cursor query.Cursor, limit uint64, direction query.Ordering) []*treasury.Record { - var start uint64 - - start = 0 - if direction == query.Descending { - start = s.last + 1 - } - if len(cursor) > 0 { - start = cursor.ToUint64() - } - - var res []*treasury.Record - for _, item := range items { - if item.Id > start && direction == query.Ascending { - res = append(res, item) - } - if item.Id < start && direction == query.Descending { - res = append(res, item) - } - } - - if direction == query.Descending { - sort.Sort(sort.Reverse(ById(res))) - } - - if len(res) >= int(limit) { - return res[:limit] - } - - return res -} - -func (s *store) findFunding(data *treasury.FundingHistoryRecord) *treasury.FundingHistoryRecord { - for _, item := range s.fundingRecords { - if item.Id == data.Id { - return item - } - if data.TransactionId == item.TransactionId { - return item - } - } - return nil -} - -func (s *store) findFundingByVault(vault string) []*treasury.FundingHistoryRecord { - res := make([]*treasury.FundingHistoryRecord, 0) - for _, item := range s.fundingRecords { - if item.Vault == vault { - res = append(res, item) - } - } - return res -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.treasuryPoolRecords = nil - s.fundingRecords = nil - s.last = 0 -} diff --git a/pkg/code/data/treasury/memory/store_test.go b/pkg/code/data/treasury/memory/store_test.go deleted file mode 100644 index f93cdc4e..00000000 --- a/pkg/code/data/treasury/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/treasury/tests" -) - -func TestTreasuryPoolMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/treasury/postgres/model.go b/pkg/code/data/treasury/postgres/model.go deleted file mode 100644 index 4b913f42..00000000 --- a/pkg/code/data/treasury/postgres/model.go +++ /dev/null @@ -1,373 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "errors" - "time" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/treasury" - pgutil "github.com/code-payments/code-server/pkg/database/postgres" - q "github.com/code-payments/code-server/pkg/database/query" -) - -const ( - treasuryPoolTableName = "codewallet__core_treasurypool" - recentRootTableName = "codewallet__core_treasurypoolrecentroot" - fundingTableName = "codewallet__core_treasurypoolfunding" -) - -type treasuryPoolModel struct { - Id sql.NullInt64 `db:"id"` - - Vm string `db:"vm"` - - Name string `db:"name"` - - Address string `db:"address"` - Bump uint `db:"bump"` - - Vault string `db:"vault"` - VaultBump uint `db:"vault_bump"` - - Authority string `db:"authority"` - - MerkleTreeLevels uint `db:"merkle_tree_levels"` - - CurrentIndex uint `db:"current_index"` - HistoryListSize uint `db:"history_list_size"` - HistoryList []*recentRoot - - SolanaBlock uint64 `db:"solana_block"` - - State uint `db:"state"` - - LastUpdatedAt time.Time `db:"last_updated_at"` -} - -type recentRoot struct { - Id sql.NullInt64 `db:"id"` - Pool string `db:"pool"` - Index uint `db:"index"` - RecentRoot string `db:"recent_root"` - AtSolanaBlock uint64 `db:"at_solana_block"` // explicitly keeping a history of recent root state -} - -type fundingModel struct { - Id sql.NullInt64 `db:"id"` - Vault string `db:"vault"` - DeltaQuarks int64 `db:"delta_quarks"` - TransactionId string `db:"transaction_id"` - State uint `db:"state"` - CreatedAt time.Time `db:"created_at"` -} - -func toTreasuryPoolModel(obj *treasury.Record) (*treasuryPoolModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - historyList := make([]*recentRoot, obj.HistoryListSize) - for i, historyItem := range obj.HistoryList { - historyList[i] = &recentRoot{ - Pool: obj.Address, - Index: uint(i), - RecentRoot: historyItem, - AtSolanaBlock: obj.SolanaBlock, - } - } - - return &treasuryPoolModel{ - Vm: obj.Vm, - - Name: obj.Name, - - Address: obj.Address, - Bump: uint(obj.Bump), - - Vault: obj.Vault, - VaultBump: uint(obj.VaultBump), - - Authority: obj.Authority, - - MerkleTreeLevels: uint(obj.MerkleTreeLevels), - - CurrentIndex: uint(obj.CurrentIndex), - HistoryListSize: uint(obj.HistoryListSize), - HistoryList: historyList, - - SolanaBlock: obj.SolanaBlock, - - State: uint(obj.State), - - LastUpdatedAt: obj.LastUpdatedAt, - }, nil -} - -func fromTreasuryPoolModel(obj *treasuryPoolModel) *treasury.Record { - historyList := make([]string, obj.HistoryListSize) - for _, historyItem := range obj.HistoryList { - historyList[historyItem.Index] = historyItem.RecentRoot - } - - return &treasury.Record{ - Id: uint64(obj.Id.Int64), - - Vm: obj.Vm, - - Name: obj.Name, - - Address: obj.Address, - Bump: uint8(obj.Bump), - - Vault: obj.Vault, - VaultBump: uint8(obj.VaultBump), - - Authority: obj.Authority, - - MerkleTreeLevels: uint8(obj.MerkleTreeLevels), - - CurrentIndex: uint8(obj.CurrentIndex), - HistoryListSize: uint8(obj.HistoryListSize), - HistoryList: historyList, - - SolanaBlock: obj.SolanaBlock, - - State: treasury.TreasuryPoolState(obj.State), - - LastUpdatedAt: obj.LastUpdatedAt, - } -} - -func (m *treasuryPoolModel) dbSave(ctx context.Context, db *sqlx.DB) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - m.LastUpdatedAt = time.Now() - - query := `INSERT INTO ` + treasuryPoolTableName + ` - (vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) - ON CONFLICT (address) - DO UPDATE - SET current_index = $9, solana_block = $11, last_updated_at = $13 - WHERE ` + treasuryPoolTableName + `.address = $3 AND ` + treasuryPoolTableName + `.vault = $5 AND ` + treasuryPoolTableName + `.solana_block < $11 - RETURNING id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at - ` - err := tx.QueryRowxContext( - ctx, - query, - m.Vm, - m.Name, - m.Address, - m.Bump, - m.Vault, - m.VaultBump, - m.Authority, - m.MerkleTreeLevels, - m.CurrentIndex, - m.HistoryListSize, - m.SolanaBlock, - m.State, - m.LastUpdatedAt.UTC(), - ).StructScan(m) - if err != nil { - return pgutil.CheckNoRows(err, treasury.ErrStaleTreasuryPoolState) - } - - query = `INSERT INTO ` + recentRootTableName + ` - (pool, index, recent_root, at_solana_block) - VALUES ($1,$2,$3,$4) - RETURNING id, pool, index, recent_root, at_solana_block - ` - for _, historyItem := range m.HistoryList { - err := tx.QueryRowxContext( - ctx, - query, - historyItem.Pool, - historyItem.Index, - historyItem.RecentRoot, - historyItem.AtSolanaBlock, - ).StructScan(historyItem) - if err != nil { - return err - } - } - - return nil - }) -} - -func (m *treasuryPoolModel) populateHistoryList(ctx context.Context, db *sqlx.DB) error { - query := `SELECT id, pool, index, recent_root, at_solana_block FROM ` + recentRootTableName + ` - WHERE pool = $1 AND at_solana_block = $2 - ORDER BY index ASC - ` - err := db.SelectContext(ctx, &m.HistoryList, query, m.Address, m.SolanaBlock) - if err != nil { - return err - } - - if len(m.HistoryList) != int(m.HistoryListSize) { - return errors.New("unexpected db inconsistency") - } - - return nil -} - -func toFundingModel(obj *treasury.FundingHistoryRecord) (*fundingModel, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &fundingModel{ - Vault: obj.Vault, - DeltaQuarks: obj.DeltaQuarks, - TransactionId: obj.TransactionId, - State: uint(obj.State), - CreatedAt: obj.CreatedAt, - }, nil -} - -func fromFundingModel(m *fundingModel) *treasury.FundingHistoryRecord { - return &treasury.FundingHistoryRecord{ - Id: uint64(m.Id.Int64), - Vault: m.Vault, - DeltaQuarks: m.DeltaQuarks, - TransactionId: m.TransactionId, - State: treasury.FundingState(m.State), - CreatedAt: m.CreatedAt, - } -} - -func (m *fundingModel) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + fundingTableName + ` - (vault, delta_quarks, transaction_id, state, created_at) - VALUES ($1,$2,$3,$4,$5) - ON CONFLICT (transaction_id) - DO UPDATE - SET state = $4 - WHERE ` + fundingTableName + `.transaction_id = $3 - RETURNING id, vault, delta_quarks, transaction_id, state, created_at - ` - - return db.QueryRowxContext( - ctx, - query, - m.Vault, - m.DeltaQuarks, - m.TransactionId, - m.State, - m.CreatedAt, - ).StructScan(m) -} - -func dbGetByName(ctx context.Context, db *sqlx.DB, name string) (*treasuryPoolModel, error) { - var res treasuryPoolModel - query := `SELECT id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` - WHERE name = $1 - ` - err := db.GetContext(ctx, &res, query, name) - if err != nil { - return nil, pgutil.CheckNoRows(err, treasury.ErrTreasuryPoolNotFound) - } - - err = res.populateHistoryList(ctx, db) - if err != nil { - return nil, err - } - - return &res, nil -} - -func dbGetByAddress(ctx context.Context, db *sqlx.DB, address string) (*treasuryPoolModel, error) { - var res treasuryPoolModel - query := `SELECT id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` - WHERE address = $1 - ` - err := db.GetContext(ctx, &res, query, address) - if err != nil { - return nil, pgutil.CheckNoRows(err, treasury.ErrTreasuryPoolNotFound) - } - - err = res.populateHistoryList(ctx, db) - if err != nil { - return nil, err - } - - return &res, nil -} - -func dbGetByVault(ctx context.Context, db *sqlx.DB, vault string) (*treasuryPoolModel, error) { - var res treasuryPoolModel - query := `SELECT id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at FROM ` + treasuryPoolTableName + ` - WHERE vault = $1 - ` - err := db.GetContext(ctx, &res, query, vault) - if err != nil { - return nil, pgutil.CheckNoRows(err, treasury.ErrTreasuryPoolNotFound) - } - - err = res.populateHistoryList(ctx, db) - if err != nil { - return nil, err - } - - return &res, nil -} - -func dbGetAllByState(ctx context.Context, db *sqlx.DB, state treasury.TreasuryPoolState, cursor q.Cursor, limit uint64, direction q.Ordering) ([]*treasuryPoolModel, error) { - res := []*treasuryPoolModel{} - - query := `SELECT id, vm, name, address, bump, vault, vault_bump, authority, merkle_tree_levels, current_index, history_list_size, solana_block, state, last_updated_at - FROM ` + treasuryPoolTableName + ` - WHERE (state = $1) - ` - - opts := []interface{}{state} - query, opts = q.PaginateQuery(query, opts, cursor, limit, direction) - - err := db.SelectContext(ctx, &res, query, opts...) - if err != nil { - return nil, pgutil.CheckNoRows(err, treasury.ErrTreasuryPoolNotFound) - } - - if len(res) == 0 { - return nil, treasury.ErrTreasuryPoolNotFound - } - - for _, m := range res { - err = m.populateHistoryList(ctx, db) - if err != nil { - return nil, err - } - } - - return res, nil -} - -func dbGetTotalAvailableFunds(ctx context.Context, db *sqlx.DB, vault string) (uint64, error) { - var res int64 - - query := `SELECT - (SELECT COALESCE(SUM(delta_quarks), 0) FROM ` + fundingTableName + ` WHERE vault = $1 AND delta_quarks > 0 AND state = $2) + - (SELECT COALESCE(SUM(delta_quarks), 0) FROM ` + fundingTableName + ` WHERE vault = $1 AND delta_quarks < 0 AND state != $3); - ` - - err := db.GetContext( - ctx, - &res, - query, - vault, - treasury.FundingStateConfirmed, - treasury.FundingStateFailed, - ) - if err != nil { - return 0, pgutil.CheckNoRows(err, treasury.ErrTreasuryPoolNotFound) - } - - if res < 0 { - return 0, treasury.ErrNegativeFunding - } - return uint64(res), nil -} diff --git a/pkg/code/data/treasury/postgres/store.go b/pkg/code/data/treasury/postgres/store.go deleted file mode 100644 index 39f9770f..00000000 --- a/pkg/code/data/treasury/postgres/store.go +++ /dev/null @@ -1,105 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/treasury" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres-backed treasury.Store -func New(db *sql.DB) treasury.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Save implements treasury.Store.Save -func (s *store) Save(ctx context.Context, record *treasury.Record) error { - model, err := toTreasuryPoolModel(record) - if err != nil { - return err - } - - if err := model.dbSave(ctx, s.db); err != nil { - return err - } - - res := fromTreasuryPoolModel(model) - res.CopyTo(record) - - return nil -} - -// GetByName implements treasury.Store.GetByName -func (s *store) GetByName(ctx context.Context, name string) (*treasury.Record, error) { - model, err := dbGetByName(ctx, s.db, name) - if err != nil { - return nil, err - } - - return fromTreasuryPoolModel(model), nil -} - -// GetByAddress implements treasury.Store.GetByAddress -func (s *store) GetByAddress(ctx context.Context, address string) (*treasury.Record, error) { - model, err := dbGetByAddress(ctx, s.db, address) - if err != nil { - return nil, err - } - - return fromTreasuryPoolModel(model), nil -} - -// GetByVault implements treasury.Store.GetByVault -func (s *store) GetByVault(ctx context.Context, vault string) (*treasury.Record, error) { - model, err := dbGetByVault(ctx, s.db, vault) - if err != nil { - return nil, err - } - - return fromTreasuryPoolModel(model), nil -} - -// GetAllByState implements treasury.Store.GetAllByState -func (s *store) GetAllByState(ctx context.Context, state treasury.TreasuryPoolState, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*treasury.Record, error) { - models, err := dbGetAllByState(ctx, s.db, state, cursor, limit, direction) - if err != nil { - return nil, err - } - - res := make([]*treasury.Record, len(models)) - for i, model := range models { - res[i] = fromTreasuryPoolModel(model) - } - return res, nil -} - -// SaveFunding implements treasury.Store.SaveFunding -func (s *store) SaveFunding(ctx context.Context, record *treasury.FundingHistoryRecord) error { - model, err := toFundingModel(record) - if err != nil { - return err - } - - if err := model.dbSave(ctx, s.db); err != nil { - return err - } - - res := fromFundingModel(model) - res.CopyTo(record) - - return nil -} - -// GetTotalAvailableFunds implements treasury.Store.GetTotalAvailableFunds -func (s *store) GetTotalAvailableFunds(ctx context.Context, vault string) (uint64, error) { - return dbGetTotalAvailableFunds(ctx, s.db, vault) -} diff --git a/pkg/code/data/treasury/postgres/store_test.go b/pkg/code/data/treasury/postgres/store_test.go deleted file mode 100644 index aca5cca6..00000000 --- a/pkg/code/data/treasury/postgres/store_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/code/data/treasury/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_treasurypool( - id SERIAL NOT NULL PRIMARY KEY, - - vm TEXT NOT NULL, - - name TEXT NOT NULL, - - address TEXT NOT NULL, - bump INTEGER NOT NULL, - - vault TEXT NOT NULL, - vault_bump INTEGER NOT NULL, - - authority TEXT NOT NULL, - - merkle_tree_levels INTEGER NOT NULL, - - current_index INTEGER NOT NULL, - history_list_size INTEGER NOT NULL, - - solana_block INTEGER NOT NULL, - - state INTEGER NOT NULL, - - last_updated_at TIMESTAMP WITH TIME ZONE, - - CONSTRAINT codewallet__core_treasurypool__uniq__name UNIQUE (name), - CONSTRAINT codewallet__core_treasurypool__uniq__address UNIQUE (address), - CONSTRAINT codewallet__core_treasurypool__uniq__vault UNIQUE (vault) - ); - - CREATE TABLE codewallet__core_treasurypoolrecentroot( - id SERIAL NOT NULL PRIMARY KEY, - - pool TEXT NOT NULL, - index INTEGER NOT NULL, - recent_root TEXT NOT NULL, - at_solana_block INTEGER NOT NULL, - - CONSTRAINT codewallet__core_treasurypoolrecentroot__uniq__pool__and__index__and__at_solana_block UNIQUE (pool, index, at_solana_block) - ); - - CREATE TABLE codewallet__core_treasurypoolfunding( - id SERIAL NOT NULL PRIMARY KEY, - - vault TEXT NOT NULL, - delta_quarks BIGINT NOT NULL, - transaction_id TEXT NOT NULL, - state INTEGER NOT NULL, - created_at TIMESTAMP WITH TIME ZONE, - - CONSTRAINT codewallet__core_treasurypoolfunding__uniq__transaction_id UNIQUE (transaction_id) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_treasurypool; - DROP TABLE codewallet__core_treasurypoolrecentroot; - DROP TABLE codewallet__core_treasurypoolfunding; - ` -) - -var ( - testStore treasury.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestTreasuryPoolPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/treasury/store.go b/pkg/code/data/treasury/store.go deleted file mode 100644 index 31497f71..00000000 --- a/pkg/code/data/treasury/store.go +++ /dev/null @@ -1,38 +0,0 @@ -package treasury - -import ( - "context" - "errors" - - "github.com/code-payments/code-server/pkg/database/query" -) - -var ( - ErrTreasuryPoolNotFound = errors.New("no records could be found") - ErrTreasuryPoolBlockhashNotFound = errors.New("treasury pool blockhash not found") - ErrStaleTreasuryPoolState = errors.New("treasury pool state is stale") - ErrNegativeFunding = errors.New("treasury pool has negative funding") -) - -type Store interface { - // Save saves a treasury pool account's state - Save(ctx context.Context, record *Record) error - - // GetByName gets a treasury pool account by its name - GetByName(ctx context.Context, name string) (*Record, error) - - // GetByAddress gets a treasury pool account by its address - GetByAddress(ctx context.Context, address string) (*Record, error) - - // GetByVault gets a treasury pool account by its vault address - GetByVault(ctx context.Context, vault string) (*Record, error) - - // GetAllByState gets all treasury pool accounts in the provided state - GetAllByState(ctx context.Context, state TreasuryPoolState, cursor query.Cursor, limit uint64, direction query.Ordering) ([]*Record, error) - - // SaveFunding saves a funding history record for a treasury pool vault - SaveFunding(ctx context.Context, record *FundingHistoryRecord) error - - // GetTotalAvailableFunds gets the total available funds for a treasury pool's vault - GetTotalAvailableFunds(ctx context.Context, vault string) (uint64, error) -} diff --git a/pkg/code/data/treasury/tests/tests.go b/pkg/code/data/treasury/tests/tests.go deleted file mode 100644 index b89793c7..00000000 --- a/pkg/code/data/treasury/tests/tests.go +++ /dev/null @@ -1,263 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/treasury" - "github.com/code-payments/code-server/pkg/database/query" -) - -func RunTests(t *testing.T, s treasury.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s treasury.Store){ - testTreasuryPoolHappyPath, - testGetAllByState, - testFundingHappyPath, - } { - tf(t, s) - teardown() - } -} - -func testTreasuryPoolHappyPath(t *testing.T, s treasury.Store) { - t.Run("testTreasuryPoolHappyPath", func(t *testing.T) { - ctx := context.Background() - - start := time.Now() - - expected := &treasury.Record{ - Vm: "vm", - - Name: "name", - - Address: "treasury", - Bump: 255, - - Vault: "vault", - VaultBump: 254, - - Authority: "code", - - MerkleTreeLevels: 32, - - CurrentIndex: 1, - HistoryListSize: 3, - HistoryList: []string{"root1", "root2", "root3"}, - - SolanaBlock: 3, - - State: treasury.TreasuryPoolStateAvailable, - } - cloned := expected.Clone() - - _, err := s.GetByName(ctx, expected.Name) - assert.Equal(t, treasury.ErrTreasuryPoolNotFound, err) - - _, err = s.GetByAddress(ctx, expected.Address) - assert.Equal(t, treasury.ErrTreasuryPoolNotFound, err) - - _, err = s.GetByVault(ctx, expected.Vault) - assert.Equal(t, treasury.ErrTreasuryPoolNotFound, err) - - require.NoError(t, s.Save(ctx, expected)) - assert.True(t, expected.Id > 0) - assert.True(t, expected.LastUpdatedAt.After(start)) - assertEquivalentTreasuryPoolRecords(t, cloned, expected) - - actual, err := s.GetByName(ctx, expected.Name) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - - actual, err = s.GetByAddress(ctx, expected.Address) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - - actual, err = s.GetByVault(ctx, expected.Vault) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - - for _, blockCount := range []uint64{cloned.SolanaBlock - 1, cloned.SolanaBlock} { - expected.CurrentIndex = 2 - expected.SolanaBlock = uint64(blockCount) - expected.HistoryList = []string{"root4", "root5", "root6"} - err = s.Save(ctx, expected) - assert.Equal(t, treasury.ErrStaleTreasuryPoolState, err) - - actual, err = s.GetByName(ctx, expected.Name) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - - actual, err = s.GetByAddress(ctx, expected.Address) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - - actual, err = s.GetByVault(ctx, expected.Vault) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - } - - expected.CurrentIndex = 2 - expected.SolanaBlock += 1 - cloned = expected.Clone() - require.NoError(t, s.Save(ctx, expected)) - assertEquivalentTreasuryPoolRecords(t, cloned, expected) - - actual, err = s.GetByName(ctx, expected.Name) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - - actual, err = s.GetByAddress(ctx, expected.Address) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - - actual, err = s.GetByVault(ctx, expected.Vault) - require.NoError(t, err) - assertEquivalentTreasuryPoolRecords(t, cloned, actual) - - records, err := s.GetAllByState(ctx, cloned.State, query.EmptyCursor, 10, query.Ascending) - require.NoError(t, err) - require.Len(t, records, 1) - assertEquivalentTreasuryPoolRecords(t, cloned, records[0]) - }) -} - -func testGetAllByState(t *testing.T, s treasury.Store) { - t.Run("testGetAllByState", func(t *testing.T) { - ctx := context.Background() - - _, err := s.GetAllByState(ctx, treasury.TreasuryPoolStateAvailable, query.EmptyCursor, 10, query.Ascending) - assert.Equal(t, treasury.ErrTreasuryPoolNotFound, err) - - expected := []*treasury.Record{ - {Vm: "vm", Name: "name1", Address: "treasury1", Vault: "vault1", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root1"}, SolanaBlock: 1, State: treasury.TreasuryPoolStateAvailable}, - {Vm: "vm", Name: "name2", Address: "treasury2", Vault: "vault2", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root2"}, SolanaBlock: 2, State: treasury.TreasuryPoolStateAvailable}, - {Vm: "vm", Name: "name3", Address: "treasury3", Vault: "vault3", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root3"}, SolanaBlock: 3, State: treasury.TreasuryPoolStateAvailable}, - {Vm: "vm", Name: "name4", Address: "treasury4", Vault: "vault4", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root4"}, SolanaBlock: 4, State: treasury.TreasuryPoolStateDeprecated}, - {Vm: "vm", Name: "name5", Address: "treasury5", Vault: "vault5", Authority: "code", MerkleTreeLevels: 32, CurrentIndex: 0, HistoryListSize: 1, HistoryList: []string{"root5"}, SolanaBlock: 5, State: treasury.TreasuryPoolStateDeprecated}, - } - for _, record := range expected { - require.NoError(t, s.Save(ctx, record)) - } - - _, err = s.GetAllByState(ctx, treasury.TreasuryPoolStateUnknown, query.EmptyCursor, 10, query.Ascending) - assert.Equal(t, treasury.ErrTreasuryPoolNotFound, err) - - actual, err := s.GetAllByState(ctx, treasury.TreasuryPoolStateAvailable, query.EmptyCursor, 10, query.Ascending) - require.NoError(t, err) - assert.Len(t, actual, 3) - - actual, err = s.GetAllByState(ctx, treasury.TreasuryPoolStateDeprecated, query.EmptyCursor, 10, query.Ascending) - require.NoError(t, err) - assert.Len(t, actual, 2) - - // Check items (asc) - actual, err = s.GetAllByState(ctx, treasury.TreasuryPoolStateAvailable, query.EmptyCursor, 5, query.Ascending) - require.NoError(t, err) - require.Len(t, actual, 3) - assert.Equal(t, "treasury1", actual[0].Address) - assert.Equal(t, "treasury2", actual[1].Address) - assert.Equal(t, "treasury3", actual[2].Address) - - // Check items (desc) - actual, err = s.GetAllByState(ctx, treasury.TreasuryPoolStateAvailable, query.EmptyCursor, 5, query.Descending) - require.NoError(t, err) - require.Len(t, actual, 3) - assert.Equal(t, "treasury3", actual[0].Address) - assert.Equal(t, "treasury2", actual[1].Address) - assert.Equal(t, "treasury1", actual[2].Address) - - // Check items (asc + limit) - actual, err = s.GetAllByState(ctx, treasury.TreasuryPoolStateAvailable, query.EmptyCursor, 2, query.Ascending) - require.NoError(t, err) - require.Len(t, actual, 2) - assert.Equal(t, "treasury1", actual[0].Address) - assert.Equal(t, "treasury2", actual[1].Address) - - // Check items (desc + limit) - actual, err = s.GetAllByState(ctx, treasury.TreasuryPoolStateAvailable, query.EmptyCursor, 2, query.Descending) - require.NoError(t, err) - require.Len(t, actual, 2) - assert.Equal(t, "treasury3", actual[0].Address) - assert.Equal(t, "treasury2", actual[1].Address) - - // Check items (asc + cursor) - actual, err = s.GetAllByState(ctx, treasury.TreasuryPoolStateAvailable, query.ToCursor(1), 5, query.Ascending) - require.NoError(t, err) - require.Len(t, actual, 2) - assert.Equal(t, "treasury2", actual[0].Address) - assert.Equal(t, "treasury3", actual[1].Address) - - // Check items (desc + cursor) - actual, err = s.GetAllByState(ctx, treasury.TreasuryPoolStateAvailable, query.ToCursor(3), 5, query.Descending) - require.NoError(t, err) - require.Len(t, actual, 2) - assert.Equal(t, "treasury2", actual[0].Address) - assert.Equal(t, "treasury1", actual[1].Address) - }) -} - -func testFundingHappyPath(t *testing.T, s treasury.Store) { - t.Run("testFundingHappyPath", func(t *testing.T) { - ctx := context.Background() - - actual, err := s.GetTotalAvailableFunds(ctx, "vault1") - require.NoError(t, err) - assert.EqualValues(t, 0, actual) - - records := []*treasury.FundingHistoryRecord{ - {Vault: "vault1", DeltaQuarks: 1, TransactionId: "txn1", State: treasury.FundingStateUnknown, CreatedAt: time.Now()}, - {Vault: "vault1", DeltaQuarks: 10, TransactionId: "txn2", State: treasury.FundingStatePending, CreatedAt: time.Now()}, - {Vault: "vault1", DeltaQuarks: 100, TransactionId: "txn3", State: treasury.FundingStateFailed, CreatedAt: time.Now()}, - {Vault: "vault1", DeltaQuarks: 1000, TransactionId: "txn4", State: treasury.FundingStateConfirmed, CreatedAt: time.Now()}, - {Vault: "vault1", DeltaQuarks: -1, TransactionId: "txn5", State: treasury.FundingStateUnknown, CreatedAt: time.Now()}, - {Vault: "vault1", DeltaQuarks: -10, TransactionId: "txn6", State: treasury.FundingStatePending, CreatedAt: time.Now()}, - {Vault: "vault1", DeltaQuarks: -100, TransactionId: "txn7", State: treasury.FundingStateConfirmed, CreatedAt: time.Now()}, - {Vault: "vault1", DeltaQuarks: -1000, TransactionId: "txn8", State: treasury.FundingStateFailed, CreatedAt: time.Now()}, - - {Vault: "vault2", DeltaQuarks: -1, TransactionId: "txn9", State: treasury.FundingStateConfirmed, CreatedAt: time.Now()}, - - {Vault: "vault3", DeltaQuarks: 1000, TransactionId: "txn10", State: treasury.FundingStateConfirmed, CreatedAt: time.Now()}, - } - - for _, record := range records { - require.NoError(t, s.SaveFunding(ctx, record)) - } - - actual, err = s.GetTotalAvailableFunds(ctx, "vault1") - require.NoError(t, err) - assert.EqualValues(t, 889, actual) - - _, err = s.GetTotalAvailableFunds(ctx, "vault2") - assert.Equal(t, treasury.ErrNegativeFunding, err) - - actual, err = s.GetTotalAvailableFunds(ctx, "vault3") - require.NoError(t, err) - assert.EqualValues(t, 1000, actual) - }) -} - -func assertEquivalentTreasuryPoolRecords(t *testing.T, obj1, obj2 *treasury.Record) { - assert.Equal(t, obj1.Name, obj2.Name) - - assert.Equal(t, obj1.Address, obj2.Address) - assert.Equal(t, obj1.Bump, obj2.Bump) - - assert.Equal(t, obj1.Vault, obj2.Vault) - assert.Equal(t, obj1.VaultBump, obj2.VaultBump) - - assert.Equal(t, obj1.Authority, obj2.Authority) - - assert.Equal(t, obj1.MerkleTreeLevels, obj2.MerkleTreeLevels) - - assert.Equal(t, obj1.CurrentIndex, obj2.CurrentIndex) - assert.Equal(t, obj1.HistoryListSize, obj2.HistoryListSize) - assert.EqualValues(t, obj1.HistoryList, obj2.HistoryList) - - assert.Equal(t, obj1.SolanaBlock, obj2.SolanaBlock) - - assert.Equal(t, obj1.State, obj2.State) -} diff --git a/pkg/code/data/treasury/treasury.go b/pkg/code/data/treasury/treasury.go deleted file mode 100644 index 165e2b49..00000000 --- a/pkg/code/data/treasury/treasury.go +++ /dev/null @@ -1,308 +0,0 @@ -package treasury - -import ( - "bytes" - "time" - - "github.com/mr-tron/base58" - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/solana/cvm" -) - -type TreasuryPoolState uint8 -type FundingState uint8 - -const ( - TreasuryPoolStateUnknown TreasuryPoolState = iota - TreasuryPoolStateAvailable - TreasuryPoolStateDeprecated -) - -const ( - FundingStateUnknown FundingState = iota - FundingStatePending - FundingStateConfirmed - FundingStateFailed -) - -type Record struct { - Id uint64 - - Vm string - - Name string - - Address string - Bump uint8 - - Vault string - VaultBump uint8 - - Authority string - - MerkleTreeLevels uint8 - - CurrentIndex uint8 - HistoryListSize uint8 - HistoryList []string // order maintained with on-chain state - - SolanaBlock uint64 - - State TreasuryPoolState // currently managed manually - - LastUpdatedAt time.Time -} - -type FundingHistoryRecord struct { - Id uint64 - Vault string - DeltaQuarks int64 - TransactionId string - State FundingState - CreatedAt time.Time -} - -func (r *Record) GetMostRecentRoot() string { - return r.HistoryList[r.CurrentIndex] -} - -func (r *Record) GetPreviousMostRecentRoot() string { - previousIndex := r.CurrentIndex - 1 - if r.CurrentIndex == 0 { - previousIndex = r.HistoryListSize - 1 - } - - return r.HistoryList[previousIndex] -} - -func (r *Record) ContainsRecentRoot(recentRoot string) (bool, int) { - for i, historyItem := range r.HistoryList { - if historyItem == recentRoot { - deltaFromMostRecent := int(r.CurrentIndex) - i - if deltaFromMostRecent < 0 { - deltaFromMostRecent += int(r.HistoryListSize) - } - - return true, deltaFromMostRecent - } - } - return false, 0 -} - -func (r *Record) Update(data *cvm.RelayAccount, solanaBlock uint64) error { - // Sanity check we're updating the right record by computing and checking - // the expected vault address - - addressBytes, err := base58.Decode(r.Address) - if err != nil { - return errors.Wrap(err, "error decoding address") - } - - vaultAddressBytes, _, err := cvm.GetRelayDestinationAddress(&cvm.GetRelayDestinationAddressArgs{ - RelayOrProof: addressBytes, - }) - if err != nil { - return errors.Wrap(err, "error getting vault address") - } - - if !bytes.Equal(vaultAddressBytes, data.Treasury.Vault) { - return errors.New("updating wrong pool record") - } - - // Check to see if there are any actual updates to the treasury pool state - - if solanaBlock <= r.SolanaBlock { - return ErrStaleTreasuryPoolState - } - - if r.CurrentIndex == data.RecentRoots.Offset { - var hasUpdatedHistoryList bool - for i := 0; i < len(data.RecentRoots.Items); i++ { - if r.HistoryList[i] != data.RecentRoots.Items[i].String() { - hasUpdatedHistoryList = true - break - } - } - - if !hasUpdatedHistoryList { - return ErrStaleTreasuryPoolState - } - } - - // It's now safe to update the record - - r.CurrentIndex = data.RecentRoots.Offset - - historyList := make([]string, r.HistoryListSize) - for i, recentRoot := range data.RecentRoots.Items { - historyList[i] = recentRoot.String() - } - for i, hash := range historyList { - if len(hash) == 0 { - historyList[i] = historyList[0] - } - } - r.HistoryList = historyList - - r.SolanaBlock = solanaBlock - - return nil -} - -func (r *Record) Validate() error { - if len(r.Vm) == 0 { - return errors.New("vm is required") - } - - if len(r.Name) == 0 { - return errors.New("name is required") - } - - if len(r.Address) == 0 { - return errors.New("address is required") - } - - if len(r.Vault) == 0 { - return errors.New("vault is required") - } - - if len(r.Authority) == 0 { - return errors.New("authority is required") - } - - if r.MerkleTreeLevels == 0 { - return errors.New("merkle tree levels is required") - } - - if r.HistoryListSize == 0 { - return errors.New("history list size is required") - } - - if r.CurrentIndex >= r.HistoryListSize { - return errors.New("current index must be less than history list size") - } - - if len(r.HistoryList) != int(r.HistoryListSize) { - return errors.New("history list length must be equal to history list size") - } - - for _, historyItem := range r.HistoryList { - if len(historyItem) == 0 { - return errors.New("history list values are required") - } - } - - return nil -} - -func (r *Record) Clone() *Record { - historyList := make([]string, len(r.HistoryList)) - copy(historyList, r.HistoryList) - - return &Record{ - Id: r.Id, - - Vm: r.Vm, - - Name: r.Name, - - Address: r.Address, - Bump: r.Bump, - - Vault: r.Vault, - VaultBump: r.VaultBump, - - Authority: r.Authority, - - MerkleTreeLevels: r.MerkleTreeLevels, - - CurrentIndex: r.CurrentIndex, - HistoryListSize: r.HistoryListSize, - HistoryList: historyList, - - SolanaBlock: r.SolanaBlock, - - State: r.State, - - LastUpdatedAt: r.LastUpdatedAt, - } -} - -func (r *Record) CopyTo(dst *Record) { - dst.Id = r.Id - - dst.Vm = r.Vm - - dst.Name = r.Name - - dst.Address = r.Address - dst.Bump = r.Bump - - dst.Vault = r.Vault - dst.VaultBump = r.VaultBump - - dst.Authority = r.Authority - - dst.MerkleTreeLevels = r.MerkleTreeLevels - - dst.CurrentIndex = r.CurrentIndex - dst.HistoryListSize = r.HistoryListSize - dst.HistoryList = r.HistoryList - - dst.SolanaBlock = r.SolanaBlock - - dst.State = r.State - - dst.LastUpdatedAt = r.LastUpdatedAt -} - -func (s TreasuryPoolState) String() string { - switch s { - case TreasuryPoolStateAvailable: - return "available" - case TreasuryPoolStateDeprecated: - return "deprecated" - } - return "unknown" -} - -func (r *FundingHistoryRecord) Validate() error { - if len(r.Vault) == 0 { - return errors.New("vault is required") - } - - if r.DeltaQuarks == 0 { - return errors.New("quark delta is required") - } - - if len(r.TransactionId) == 0 { - return errors.New("transaction id is required") - } - - if r.CreatedAt.IsZero() { - return errors.New("creation time is zero") - } - - return nil -} - -func (r *FundingHistoryRecord) Clone() *FundingHistoryRecord { - return &FundingHistoryRecord{ - Id: r.Id, - Vault: r.Vault, - DeltaQuarks: r.DeltaQuarks, - TransactionId: r.TransactionId, - State: r.State, - CreatedAt: r.CreatedAt, - } -} - -func (r *FundingHistoryRecord) CopyTo(dst *FundingHistoryRecord) { - dst.Id = r.Id - dst.Vault = r.Vault - dst.DeltaQuarks = r.DeltaQuarks - dst.TransactionId = r.TransactionId - dst.State = r.State - dst.CreatedAt = r.CreatedAt -} diff --git a/pkg/code/data/treasury/treasury_test.go b/pkg/code/data/treasury/treasury_test.go deleted file mode 100644 index 11e4136f..00000000 --- a/pkg/code/data/treasury/treasury_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package treasury - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRecordRootUtilities(t *testing.T) { - record := &Record{ - HistoryListSize: 5, - HistoryList: []string{ - "root1", - "root2", - "root3", - "root4", - "root5", - }, - CurrentIndex: 0, - } - - assert.Equal(t, "root1", record.GetMostRecentRoot()) - assert.Equal(t, "root5", record.GetPreviousMostRecentRoot()) - - record.CurrentIndex = 2 - assert.Equal(t, "root3", record.GetMostRecentRoot()) - assert.Equal(t, "root2", record.GetPreviousMostRecentRoot()) - - record.CurrentIndex = 1 - - reorderedFromMostRecentRoot := []string{ - "root2", - "root1", - "root5", - "root4", - "root3", - } - for i, recentRoot := range reorderedFromMostRecentRoot { - containsRecentRoot, deltaFromMostRecent := record.ContainsRecentRoot(recentRoot) - assert.True(t, containsRecentRoot) - assert.Equal(t, i, deltaFromMostRecent) - } - - containsRecentRoot, _ := record.ContainsRecentRoot("root6") - assert.False(t, containsRecentRoot) -} diff --git a/pkg/code/data/twitter/memory/store.go b/pkg/code/data/twitter/memory/store.go deleted file mode 100644 index 63ba78eb..00000000 --- a/pkg/code/data/twitter/memory/store.go +++ /dev/null @@ -1,217 +0,0 @@ -package memory - -import ( - "context" - "sort" - "strings" - "sync" - "time" - - "github.com/google/uuid" - - "github.com/code-payments/code-server/pkg/code/data/twitter" -) - -type ByLastUpdatedAt []*twitter.Record - -func (a ByLastUpdatedAt) Len() int { return len(a) } -func (a ByLastUpdatedAt) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByLastUpdatedAt) Less(i, j int) bool { return a[i].LastUpdatedAt.Before(a[j].LastUpdatedAt) } - -type store struct { - mu sync.Mutex - userRecords []*twitter.Record - processedTweets map[string]any - usedNonces map[string]any - last uint64 -} - -// New returns a new in memory twitter.Store -func New() twitter.Store { - return &store{ - processedTweets: make(map[string]any), - usedNonces: make(map[string]any), - } -} - -// SaveUser implements twitter.Store.SaveUser -func (s *store) SaveUser(_ context.Context, data *twitter.Record) error { - if err := data.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.last++ - - itemByTipAddress := s.findUserByTipAddress(data.TipAddress) - if itemByTipAddress != nil && data.Username != itemByTipAddress.Username { - return twitter.ErrDuplicateTipAddress - } - - if item := s.findUser(data); item != nil { - data.LastUpdatedAt = time.Now() - - item.Name = data.Name - item.ProfilePicUrl = data.ProfilePicUrl - item.VerifiedType = data.VerifiedType - item.FollowerCount = data.FollowerCount - item.TipAddress = data.TipAddress - item.LastUpdatedAt = data.LastUpdatedAt - } else { - if data.Id == 0 { - data.Id = s.last - } - if data.CreatedAt.IsZero() { - data.CreatedAt = time.Now() - } - data.LastUpdatedAt = time.Now() - - c := data.Clone() - s.userRecords = append(s.userRecords, &c) - } - - return nil -} - -// GetUserByUsername implements twitter.Store.GetUserByUsername -func (s *store) GetUserByUsername(_ context.Context, username string) (*twitter.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findUserByUsername(username) - if item == nil { - return nil, twitter.ErrUserNotFound - } - - cloned := item.Clone() - return &cloned, nil -} - -// GetUserByTipAddress implements twitter.Store.GetUserByTipAddress -func (s *store) GetUserByTipAddress(ctx context.Context, tipAddress string) (*twitter.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - item := s.findUserByTipAddress(tipAddress) - if item == nil { - return nil, twitter.ErrUserNotFound - } - - cloned := item.Clone() - return &cloned, nil -} - -// GetStaleUsers implements twitter.Store.GetStaleUsers -func (s *store) GetStaleUsers(ctx context.Context, minAge time.Duration, limit int) ([]*twitter.Record, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items := s.findStaleUsers(minAge) - - sorted := ByLastUpdatedAt(items) - sort.Sort(sorted) - - if len(items) > limit { - sorted = sorted[:limit] - } - - if len(sorted) == 0 { - return nil, twitter.ErrUserNotFound - } - return userSliceCopy(sorted), nil -} - -// MarkTweetAsProcessed implements twitter.Store.MarkTweetAsProcessed -func (s *store) MarkTweetAsProcessed(_ context.Context, tweetId string) error { - s.mu.Lock() - defer s.mu.Unlock() - - s.processedTweets[tweetId] = struct{}{} - return nil -} - -// IsTweetProcessed implements twitter.Store.IsTweetProcessed -func (s *store) IsTweetProcessed(_ context.Context, tweetId string) (bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - - _, ok := s.processedTweets[tweetId] - return ok, nil -} - -func (s *store) MarkNonceAsUsed(_ context.Context, _ string, nonce uuid.UUID) error { - s.mu.Lock() - defer s.mu.Unlock() - - _, ok := s.usedNonces[nonce.String()] - if ok { - return twitter.ErrDuplicateNonce - } - - s.usedNonces[nonce.String()] = struct{}{} - return nil -} - -func (s *store) findUser(data *twitter.Record) *twitter.Record { - for _, item := range s.userRecords { - if item.Id == data.Id { - return item - } - if strings.EqualFold(data.Username, item.Username) { - return item - } - } - - return nil -} - -func (s *store) findUserByUsername(username string) *twitter.Record { - for _, item := range s.userRecords { - if strings.EqualFold(username, item.Username) { - return item - } - } - - return nil -} - -func (s *store) findUserByTipAddress(tipAddress string) *twitter.Record { - for _, item := range s.userRecords { - if tipAddress == item.TipAddress { - return item - } - } - - return nil -} - -func (s *store) findStaleUsers(minAge time.Duration) []*twitter.Record { - var res []*twitter.Record - for _, item := range s.userRecords { - if time.Since(item.LastUpdatedAt) > minAge { - res = append(res, item) - } - } - return res -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.userRecords = nil - s.processedTweets = make(map[string]any) - s.usedNonces = make(map[string]any) - s.last = 0 -} - -func userSliceCopy(items []*twitter.Record) []*twitter.Record { - res := make([]*twitter.Record, len(items)) - for i, item := range items { - cloned := item.Clone() - res[i] = &cloned - } - return res -} diff --git a/pkg/code/data/twitter/memory/store_test.go b/pkg/code/data/twitter/memory/store_test.go deleted file mode 100644 index 2051d48f..00000000 --- a/pkg/code/data/twitter/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/twitter/tests" -) - -func TestTwitterMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/twitter/postgres/model.go b/pkg/code/data/twitter/postgres/model.go deleted file mode 100644 index 891f6124..00000000 --- a/pkg/code/data/twitter/postgres/model.go +++ /dev/null @@ -1,222 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/google/uuid" - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-protobuf-api/generated/go/user/v1" - "github.com/code-payments/code-server/pkg/code/data/twitter" - pgutil "github.com/code-payments/code-server/pkg/database/postgres" -) - -const ( - userTableName = "codewallet__core_twitteruser" - processedTweetsTableName = "codewallet__core_processedtweets" - usedNoncesTableName = "codewallet__core_usedtwitternonces" -) - -type model struct { - Id sql.NullInt64 `db:"id"` - - Username string `db:"username"` - Name string `db:"name"` - ProfilePicUrl string `db:"profile_pic_url"` - VerifiedType uint8 `db:"verified_type"` - FollowerCount uint32 `db:"follower_count"` - - TipAddress string `db:"tip_address"` - - CreatedAt time.Time `db:"created_at"` - LastUpdatedAt time.Time `db:"last_updated_at"` -} - -func toModel(r *twitter.Record) (*model, error) { - if err := r.Validate(); err != nil { - return nil, err - } - - return &model{ - Username: r.Username, - Name: r.Name, - ProfilePicUrl: r.ProfilePicUrl, - VerifiedType: uint8(r.VerifiedType), - FollowerCount: r.FollowerCount, - - TipAddress: r.TipAddress, - - CreatedAt: r.CreatedAt, - LastUpdatedAt: r.LastUpdatedAt, - }, nil -} - -func fromModel(m *model) *twitter.Record { - return &twitter.Record{ - Id: uint64(m.Id.Int64), - - Username: m.Username, - Name: m.Name, - ProfilePicUrl: m.ProfilePicUrl, - VerifiedType: user.TwitterUser_VerifiedType(m.VerifiedType), - FollowerCount: m.FollowerCount, - - TipAddress: m.TipAddress, - - CreatedAt: m.CreatedAt, - LastUpdatedAt: m.LastUpdatedAt, - } -} - -func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `INSERT INTO ` + userTableName + ` - (username, name, profile_pic_url, verified_type, follower_count, tip_address, created_at, last_updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - - ON CONFLICT (username) - DO UPDATE - SET name = $2, profile_pic_url = $3, verified_type = $4, follower_count = $5, tip_address = $6, created_at = $7, last_updated_at = $8 - WHERE ` + userTableName + `.username = $1 - - RETURNING - id, username, name, profile_pic_url, verified_type, follower_count, tip_address, created_at, last_updated_at` - - if m.CreatedAt.IsZero() { - m.CreatedAt = time.Now() - } - m.LastUpdatedAt = time.Now() - - err := tx.QueryRowxContext( - ctx, - query, - m.Username, - m.Name, - m.ProfilePicUrl, - m.VerifiedType, - m.FollowerCount, - m.TipAddress, - m.CreatedAt, - m.LastUpdatedAt, - ).StructScan(m) - - return pgutil.CheckUniqueViolation(err, twitter.ErrDuplicateTipAddress) - }) -} - -func dbGetUserByUsername(ctx context.Context, db *sqlx.DB, username string) (*model, error) { - res := &model{} - - query := `SELECT - id, username, name, profile_pic_url, verified_type, follower_count, tip_address, created_at, last_updated_at - FROM ` + userTableName + ` - WHERE LOWER(username) = LOWER($1) - LIMIT 1` - - err := db.GetContext(ctx, res, query, username) - if err != nil { - return nil, pgutil.CheckNoRows(err, twitter.ErrUserNotFound) - } - return res, nil -} - -func dbGetUserByTipAddress(ctx context.Context, db *sqlx.DB, tipAddress string) (*model, error) { - res := &model{} - - query := `SELECT - id, username, name, profile_pic_url, verified_type, follower_count, tip_address, created_at, last_updated_at - FROM ` + userTableName + ` - WHERE tip_address = $1 - LIMIT 1` - - err := db.GetContext(ctx, res, query, tipAddress) - if err != nil { - return nil, pgutil.CheckNoRows(err, twitter.ErrUserNotFound) - } - return res, nil -} - -func dbGetStaleUsers(ctx context.Context, db *sqlx.DB, minAge time.Duration, limit int) ([]*model, error) { - res := []*model{} - - query := `SELECT - id, username, name, profile_pic_url, verified_type, follower_count, tip_address, created_at, last_updated_at - FROM ` + userTableName + ` - WHERE last_updated_at < $1 - ORDER BY last_updated_at ASC - LIMIT $2` - - err := db.SelectContext(ctx, &res, query, time.Now().Add(-1*minAge), limit) - if err != nil { - return nil, pgutil.CheckNoRows(err, twitter.ErrUserNotFound) - } - if len(res) == 0 { - return nil, twitter.ErrUserNotFound - } - return res, nil -} - -func dbMarkTweetAsProcessed(ctx context.Context, db *sqlx.DB, tweetId string) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `INSERT INTO ` + processedTweetsTableName + ` - (tweet_id, created_at) - VALUES ($1, $2) - - ON CONFLICT (tweet_id) DO NOTHING - - RETURNING - id, tweet_id, created_at` - - _, err := tx.ExecContext( - ctx, - query, - tweetId, - time.Now(), - ) - return pgutil.CheckNoRows(err, nil) - }) -} - -func dbIsTweetProcessed(ctx context.Context, db *sqlx.DB, tweetId string) (bool, error) { - res := struct { - Count int `db:"count"` - }{} - - query := `SELECT COUNT(*) AS count - FROM ` + processedTweetsTableName + ` - WHERE tweet_id = $1 - LIMIT 1` - - err := db.GetContext( - ctx, - &res, - query, - tweetId, - ) - if err != nil { - return false, err - } - return res.Count > 0, nil -} - -func dbMarkNonceAsUsed(ctx context.Context, db *sqlx.DB, tweetId string, nonce uuid.UUID) error { - return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error { - query := `INSERT INTO ` + usedNoncesTableName + ` - (tweet_id, nonce, created_at) - VALUES ($1, $2, $3) - - RETURNING - id, tweet_id, nonce, created_at` - - _, err := tx.ExecContext( - ctx, - query, - tweetId, - nonce, - time.Now(), - ) - return pgutil.CheckUniqueViolation(err, twitter.ErrDuplicateNonce) - }) -} diff --git a/pkg/code/data/twitter/postgres/store.go b/pkg/code/data/twitter/postgres/store.go deleted file mode 100644 index a9b75823..00000000 --- a/pkg/code/data/twitter/postgres/store.go +++ /dev/null @@ -1,87 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/code-payments/code-server/pkg/code/data/twitter" - "github.com/google/uuid" - "github.com/jmoiron/sqlx" -) - -type store struct { - db *sqlx.DB -} - -// New returns a new postgres twitter.Store -func New(db *sql.DB) twitter.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// SaveUser implements twitter.Store.SaveUser -func (s *store) SaveUser(ctx context.Context, record *twitter.Record) error { - model, err := toModel(record) - if err != nil { - return err - } - - err = model.dbSave(ctx, s.db) - if err != nil { - return err - } - - res := fromModel(model) - res.CopyTo(record) - - return nil -} - -// GetUserByUsername implements twitter.Store.GetUserByUsername -func (s *store) GetUserByUsername(ctx context.Context, username string) (*twitter.Record, error) { - model, err := dbGetUserByUsername(ctx, s.db, username) - if err != nil { - return nil, err - } - return fromModel(model), nil -} - -// GetUserByTipAddress implements twitter.Store.GetUserByTipAddress -func (s *store) GetUserByTipAddress(ctx context.Context, tipAddress string) (*twitter.Record, error) { - model, err := dbGetUserByTipAddress(ctx, s.db, tipAddress) - if err != nil { - return nil, err - } - return fromModel(model), nil -} - -// GetStaleUsers implements twitter.Store.GetStaleUsers -func (s *store) GetStaleUsers(ctx context.Context, minAge time.Duration, limit int) ([]*twitter.Record, error) { - models, err := dbGetStaleUsers(ctx, s.db, minAge, limit) - if err != nil { - return nil, err - } - - res := make([]*twitter.Record, len(models)) - for i, model := range models { - res[i] = fromModel(model) - } - return res, nil -} - -// MarkTweetAsProcessed implements twitter.Store.MarkTweetAsProcessed -func (s *store) MarkTweetAsProcessed(ctx context.Context, tweetId string) error { - return dbMarkTweetAsProcessed(ctx, s.db, tweetId) -} - -// IsTweetProcessed implements twitter.Store.IsTweetProcessed -func (s *store) IsTweetProcessed(ctx context.Context, tweetId string) (bool, error) { - return dbIsTweetProcessed(ctx, s.db, tweetId) -} - -// MarkNonceAsUsed implements twitter.Store.MarkNonceAsUsed -func (s *store) MarkNonceAsUsed(ctx context.Context, tweetId string, nonce uuid.UUID) error { - return dbMarkNonceAsUsed(ctx, s.db, tweetId, nonce) -} diff --git a/pkg/code/data/twitter/postgres/store_test.go b/pkg/code/data/twitter/postgres/store_test.go deleted file mode 100644 index 3f5eba8f..00000000 --- a/pkg/code/data/twitter/postgres/store_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/data/twitter" - "github.com/code-payments/code-server/pkg/code/data/twitter/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -var ( - testStore twitter.Store - teardown func() -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_twitteruser ( - id SERIAL NOT NULL PRIMARY KEY, - - username TEXT NOT NULL, - name TEXT NOT NULL, - profile_pic_url TEXT NOT NULL, - verified_type INTEGER NOT NULL, - follower_count INTEGER NOT NULL, - - tip_address TEXT NOT NULL, - - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - last_updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_twitteruser__uniq__username UNIQUE (username), - CONSTRAINT codewallet__core_twitteruser__uniq__tip_address UNIQUE (tip_address) - ); - - CREATE TABLE codewallet__core_processedtweets ( - id SERIAL NOT NULL PRIMARY KEY, - - tweet_id TEXT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_processedtweets__uniq__tweet_id UNIQUE (tweet_id) - ); - - CREATE TABLE codewallet__core_usedtwitternonces ( - id SERIAL NOT NULL PRIMARY KEY, - - nonce UUID NOT NULL, - tweet_id TEXT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - - CONSTRAINT codewallet__core_usedtwitternonces__uniq__tweet_id UNIQUE (nonce) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_twitteruser; - DROP TABLE codewallet__core_processedtweets; - DROP TABLE codewallet__core_usedtwitternonces; - ` -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestTwitterPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/twitter/store.go b/pkg/code/data/twitter/store.go deleted file mode 100644 index 5c56df38..00000000 --- a/pkg/code/data/twitter/store.go +++ /dev/null @@ -1,39 +0,0 @@ -package twitter - -import ( - "context" - "errors" - "time" - - "github.com/google/uuid" -) - -var ( - ErrUserNotFound = errors.New("twitter user not found") - ErrDuplicateTipAddress = errors.New("duplicate tip address") - ErrDuplicateNonce = errors.New("duplicate nonce") -) - -type Store interface { - // SaveUser saves a Twitter user's information - SaveUser(ctx context.Context, record *Record) error - - // GetUserByUsername gets a Twitter user's information by the username - GetUserByUsername(ctx context.Context, username string) (*Record, error) - - // GetUserByTipAddress gets a Twitter user's information by the tip address - GetUserByTipAddress(ctx context.Context, tipAddress string) (*Record, error) - - // GetStaleUsers gets user that have their last updated timestamp older than minAge - GetStaleUsers(ctx context.Context, minAge time.Duration, limit int) ([]*Record, error) - - // MarkTweetAsProcessed marks a tweet as being processed - MarkTweetAsProcessed(ctx context.Context, tweetId string) error - - // IsTweetProcessed returns whether a tweet is processed - IsTweetProcessed(ctx context.Context, tweetId string) (bool, error) - - // MarkNonceAsUsed marks a registration nonce as being used and assigned - // to the provided tweet. - MarkNonceAsUsed(ctx context.Context, tweetId string, nonce uuid.UUID) error -} diff --git a/pkg/code/data/twitter/tests/tests.go b/pkg/code/data/twitter/tests/tests.go deleted file mode 100644 index 522a2abb..00000000 --- a/pkg/code/data/twitter/tests/tests.go +++ /dev/null @@ -1,205 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "strings" - "testing" - "time" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - userpb "github.com/code-payments/code-protobuf-api/generated/go/user/v1" - - "github.com/code-payments/code-server/pkg/code/data/twitter" -) - -func RunTests(t *testing.T, s twitter.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s twitter.Store){ - testUserHappyPath, - testTweetHappyPath, - testNonceHappyPath, - testGetStaleUsers, - } { - tf(t, s) - teardown() - } -} - -func testUserHappyPath(t *testing.T, s twitter.Store) { - t.Run("testUserHappyPath", func(t *testing.T) { - ctx := context.Background() - - username := "JeffYanta" - tipAddress1 := "tip_address_1" - tipAddress2 := "tip_address_2" - - _, err := s.GetUserByUsername(ctx, username) - assert.Equal(t, twitter.ErrUserNotFound, err) - - _, err = s.GetUserByTipAddress(ctx, tipAddress1) - assert.Equal(t, twitter.ErrUserNotFound, err) - - expected := &twitter.Record{ - Username: username, - Name: "Jeff", - ProfilePicUrl: "https://pbs.twimg.com/profile_images/1728595562285441024/GM-aLyh__normal.jpg", - VerifiedType: userpb.TwitterUser_BLUE, - FollowerCount: 200, - TipAddress: tipAddress1, - } - cloned := expected.Clone() - - start := time.Now() - require.NoError(t, s.SaveUser(ctx, expected)) - assert.EqualValues(t, 1, expected.Id) - assert.True(t, expected.CreatedAt.After(start)) - assert.True(t, expected.LastUpdatedAt.After(start)) - - actual, err := s.GetUserByUsername(ctx, username) - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - - actual, err = s.GetUserByTipAddress(ctx, tipAddress1) - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - - expected.Name = "Jeff Yanta" - expected.ProfilePicUrl = "https://pbs.twimg.com/profile_images/1728595562285441024/GM-aLyh__highres.jpg" - expected.VerifiedType = userpb.TwitterUser_NONE - expected.FollowerCount = 1000 - cloned = expected.Clone() - require.NoError(t, s.SaveUser(ctx, expected)) - assert.True(t, expected.LastUpdatedAt.After(expected.CreatedAt)) - - actual, err = s.GetUserByUsername(ctx, strings.ToLower(username)) - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - - actual, err = s.GetUserByTipAddress(ctx, tipAddress1) - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - - expected.TipAddress = tipAddress2 - cloned = expected.Clone() - require.NoError(t, s.SaveUser(ctx, expected)) - assert.True(t, expected.LastUpdatedAt.After(expected.CreatedAt)) - - actual, err = s.GetUserByUsername(ctx, strings.ToUpper(username)) - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - - _, err = s.GetUserByTipAddress(ctx, tipAddress1) - assert.Equal(t, twitter.ErrUserNotFound, err) - - actual, err = s.GetUserByTipAddress(ctx, tipAddress2) - require.NoError(t, err) - assertEquivalentRecords(t, &cloned, actual) - - duplicatedTipAddressRecord := &twitter.Record{ - Username: "anotheruser", - Name: "Another User", - ProfilePicUrl: "https://pbs.twimg.com/profile_images/1728595562285441024/GM-aLyh__normal.jpg", - VerifiedType: userpb.TwitterUser_NONE, - FollowerCount: 8, - TipAddress: tipAddress2, - } - assert.Equal(t, twitter.ErrDuplicateTipAddress, s.SaveUser(ctx, duplicatedTipAddressRecord)) - }) -} - -func testTweetHappyPath(t *testing.T, s twitter.Store) { - t.Run("testTweetHappyPath", func(t *testing.T) { - ctx := context.Background() - - tweet1 := "tweet1" - tweet2 := "tweet2" - - isProcessed, err := s.IsTweetProcessed(ctx, tweet1) - require.NoError(t, err) - assert.False(t, isProcessed) - - for i := 0; i < 3; i++ { - require.NoError(t, s.MarkTweetAsProcessed(ctx, tweet1)) - - isProcessed, err = s.IsTweetProcessed(ctx, tweet1) - require.NoError(t, err) - assert.True(t, isProcessed) - - isProcessed, err = s.IsTweetProcessed(ctx, tweet2) - require.NoError(t, err) - assert.False(t, isProcessed) - } - }) -} - -func testNonceHappyPath(t *testing.T, s twitter.Store) { - t.Run("testNonceHappyPath", func(t *testing.T) { - ctx := context.Background() - - nonce := uuid.New() - - require.NoError(t, s.MarkNonceAsUsed(ctx, "tweet1", nonce)) - - for i := 0; i < 3; i++ { - assert.Equal(t, twitter.ErrDuplicateNonce, s.MarkNonceAsUsed(ctx, "tweet2", nonce)) - } - }) -} - -func testGetStaleUsers(t *testing.T, s twitter.Store) { - t.Run("testGetStaleUsers", func(t *testing.T) { - ctx := context.Background() - - _, err := s.GetStaleUsers(ctx, time.Minute, 10) - assert.Equal(t, twitter.ErrUserNotFound, err) - - start := time.Now() - delayPerUpdate := 100 * time.Millisecond - - for i := 0; i < 5; i++ { - require.NoError(t, s.SaveUser(ctx, &twitter.Record{ - Username: fmt.Sprintf("username%d", i), - Name: fmt.Sprintf("name%d", i), - ProfilePicUrl: fmt.Sprintf("profile_pic_%d", i), - VerifiedType: userpb.TwitterUser_NONE, - FollowerCount: 0, - TipAddress: fmt.Sprintf("tip_address_%d", i), - })) - - // todo: get rid of time.Sleep() - time.Sleep(delayPerUpdate) - } - - res, err := s.GetStaleUsers(ctx, 0, 10) - require.NoError(t, err) - require.Len(t, res, 5) - for i, actual := range res { - assert.Equal(t, fmt.Sprintf("username%d", i), actual.Username) - } - - res, err = s.GetStaleUsers(ctx, 0, 3) - require.NoError(t, err) - require.Len(t, res, 3) - for i, actual := range res { - assert.Equal(t, fmt.Sprintf("username%d", i), actual.Username) - } - - res, err = s.GetStaleUsers(ctx, time.Since(start)-delayPerUpdate/2, 10) - require.NoError(t, err) - require.Len(t, res, 1) - assert.Equal(t, "username0", res[0].Username) - - }) -} - -func assertEquivalentRecords(t *testing.T, obj1, obj2 *twitter.Record) { - assert.Equal(t, obj1.Username, obj2.Username) - assert.Equal(t, obj1.Name, obj2.Name) - assert.Equal(t, obj1.ProfilePicUrl, obj2.ProfilePicUrl) - assert.Equal(t, obj1.VerifiedType, obj2.VerifiedType) - assert.Equal(t, obj1.FollowerCount, obj2.FollowerCount) - assert.Equal(t, obj1.TipAddress, obj2.TipAddress) -} diff --git a/pkg/code/data/twitter/user.go b/pkg/code/data/twitter/user.go deleted file mode 100644 index e2ec47a1..00000000 --- a/pkg/code/data/twitter/user.go +++ /dev/null @@ -1,75 +0,0 @@ -package twitter - -import ( - "errors" - "time" - - userpb "github.com/code-payments/code-protobuf-api/generated/go/user/v1" -) - -type Record struct { - Id uint64 - - Username string - Name string - ProfilePicUrl string - VerifiedType userpb.TwitterUser_VerifiedType - FollowerCount uint32 - - TipAddress string - - LastUpdatedAt time.Time - CreatedAt time.Time -} - -func (r *Record) Validate() error { - if len(r.Username) == 0 { - return errors.New("username is required") - } - - if len(r.Name) == 0 { - return errors.New("name is required") - } - - if len(r.ProfilePicUrl) == 0 { - return errors.New("profile pic url is required") - } - - if len(r.TipAddress) == 0 { - return errors.New("tip address is required") - } - - return nil -} - -func (r *Record) Clone() Record { - return Record{ - Id: r.Id, - - Username: r.Username, - Name: r.Name, - ProfilePicUrl: r.ProfilePicUrl, - VerifiedType: r.VerifiedType, - FollowerCount: r.FollowerCount, - - TipAddress: r.TipAddress, - - LastUpdatedAt: r.LastUpdatedAt, - CreatedAt: r.CreatedAt, - } -} - -func (r *Record) CopyTo(dst *Record) { - dst.Id = r.Id - - dst.Username = r.Username - dst.Name = r.Name - dst.ProfilePicUrl = r.ProfilePicUrl - dst.VerifiedType = r.VerifiedType - dst.FollowerCount = r.FollowerCount - - dst.TipAddress = r.TipAddress - - dst.LastUpdatedAt = r.LastUpdatedAt - dst.CreatedAt = r.CreatedAt -} diff --git a/pkg/code/data/user/identity/memory/store.go b/pkg/code/data/user/identity/memory/store.go deleted file mode 100644 index f08f0a82..00000000 --- a/pkg/code/data/user/identity/memory/store.go +++ /dev/null @@ -1,96 +0,0 @@ -package memory - -import ( - "context" - "sync" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_identity "github.com/code-payments/code-server/pkg/code/data/user/identity" -) - -type store struct { - mu sync.RWMutex - usersByID map[string]*user_identity.Record - usersByPhoneNumber map[string]*user_identity.Record -} - -// New returns an in memory user_identity.Store -func New() user_identity.Store { - return &store{ - usersByID: make(map[string]*user_identity.Record), - usersByPhoneNumber: make(map[string]*user_identity.Record), - } -} - -// Put implements user_identity.Store.Put -func (s *store) Put(ctx context.Context, record *user_identity.Record) error { - if err := record.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - _, ok := s.usersByID[record.ID.String()] - if ok { - return user_identity.ErrAlreadyExists - } - - _, ok = s.usersByPhoneNumber[*record.View.PhoneNumber] - if ok { - return user_identity.ErrAlreadyExists - } - - copy := &user_identity.Record{ - ID: record.ID, - View: &user.View{ - PhoneNumber: record.View.PhoneNumber, - }, - IsStaffUser: record.IsStaffUser, - IsBanned: record.IsBanned, - CreatedAt: record.CreatedAt, - } - - s.usersByID[record.ID.String()] = copy - s.usersByPhoneNumber[*record.View.PhoneNumber] = copy - - return nil -} - -// GetByID implements user_identity.Store.GetByID -func (s *store) GetByID(ctx context.Context, id *user.UserID) (*user_identity.Record, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - record, ok := s.usersByID[id.String()] - if !ok { - return nil, user_identity.ErrNotFound - } - - return record, nil -} - -// GetByView implements user_identity.Store.GetByView -func (s *store) GetByView(ctx context.Context, view *user.View) (*user_identity.Record, error) { - if err := view.Validate(); err != nil { - return nil, err - } - - s.mu.RLock() - defer s.mu.RUnlock() - - record, ok := s.usersByPhoneNumber[*view.PhoneNumber] - if !ok { - return nil, user_identity.ErrNotFound - } - - return record, nil -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.usersByID = make(map[string]*user_identity.Record) - s.usersByPhoneNumber = make(map[string]*user_identity.Record) -} diff --git a/pkg/code/data/user/identity/memory/store_test.go b/pkg/code/data/user/identity/memory/store_test.go deleted file mode 100644 index 627caa06..00000000 --- a/pkg/code/data/user/identity/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/user/identity/tests" -) - -func TestUserIdentityMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/user/identity/postgres/model.go b/pkg/code/data/user/identity/postgres/model.go deleted file mode 100644 index a4c252f9..00000000 --- a/pkg/code/data/user/identity/postgres/model.go +++ /dev/null @@ -1,107 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_identity "github.com/code-payments/code-server/pkg/code/data/user/identity" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" -) - -const ( - tableName = "codewallet__core_appuser" -) - -type model struct { - ID sql.NullInt64 `db:"id"` - UserID string `db:"user_id"` - PhoneNumber string `db:"phone_number"` - IsStaffuser bool `db:"is_staff_user"` - IsBanned bool `db:"is_banned"` - CreatedAt time.Time `db:"created_at"` -} - -func toModel(obj *user_identity.Record) (*model, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - model := &model{ - UserID: obj.ID.String(), - PhoneNumber: *obj.View.PhoneNumber, - IsStaffuser: obj.IsStaffUser, - IsBanned: obj.IsBanned, - CreatedAt: obj.CreatedAt.UTC(), - } - - return model, nil -} - -func fromModel(obj *model) (*user_identity.Record, error) { - userID, err := user.GetUserIDFromString(obj.UserID) - if err != nil { - return nil, errors.Wrap(err, "error parsing user id") - } - - return &user_identity.Record{ - ID: userID, - View: &user.View{ - PhoneNumber: &obj.PhoneNumber, - }, - IsStaffUser: obj.IsStaffuser, - IsBanned: obj.IsBanned, - CreatedAt: obj.CreatedAt, - }, nil -} - -func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + tableName + ` - (user_id, phone_number, is_staff_user, is_banned, created_at) - VALUES ($1, $2, $3, $4, $5) - RETURNING id, user_id, phone_number, is_staff_user, is_banned, created_at` - - err := db.QueryRowxContext( - ctx, - query, - m.UserID, - m.PhoneNumber, - m.IsStaffuser, - m.IsBanned, - m.CreatedAt, - ).StructScan(m) - return pgutil.CheckUniqueViolation(err, user_identity.ErrAlreadyExists) -} - -func dbGetByID(ctx context.Context, db *sqlx.DB, id *user.UserID) (*model, error) { - query := `SELECT id, user_id, phone_number, is_staff_user, is_banned, created_at FROM ` + tableName + ` - WHERE user_id = $1` - - result := &model{} - err := db.GetContext(ctx, result, query, id.String()) - if err != nil { - return nil, pgutil.CheckNoRows(err, user_identity.ErrNotFound) - } - return result, nil -} - -func dbGetByView(ctx context.Context, db *sqlx.DB, view *user.View) (*model, error) { - if err := view.Validate(); err != nil { - return nil, err - } - - query := `SELECT id, user_id, phone_number, is_staff_user, is_banned, created_at FROM ` + tableName + ` - WHERE phone_number = $1` - - result := &model{} - err := db.GetContext(ctx, result, query, *view.PhoneNumber) - if err != nil { - return nil, pgutil.CheckNoRows(err, user_identity.ErrNotFound) - } - return result, nil -} diff --git a/pkg/code/data/user/identity/postgres/model_test.go b/pkg/code/data/user/identity/postgres/model_test.go deleted file mode 100644 index 497af2c9..00000000 --- a/pkg/code/data/user/identity/postgres/model_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package postgres - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_identity "github.com/code-payments/code-server/pkg/code/data/user/identity" -) - -func TestModelConversion(t *testing.T) { - phoneNumber := "+12223334444" - record := &user_identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - IsStaffUser: true, - IsBanned: true, - CreatedAt: time.Now(), - } - - model, err := toModel(record) - require.NoError(t, err) - - actual, err := fromModel(model) - require.NoError(t, err) - - assertEqualUsers(t, record, actual) -} - -func assertEqualUsers(t *testing.T, obj1, obj2 *user_identity.Record) { - require.NoError(t, obj1.Validate()) - require.NoError(t, obj2.Validate()) - - assert.Equal(t, obj1.ID, obj2.ID) - assert.Equal(t, obj1.IsStaffUser, obj2.IsStaffUser) - assert.Equal(t, obj1.IsBanned, obj2.IsBanned) - assert.Equal(t, *obj1.View.PhoneNumber, *obj2.View.PhoneNumber) - assert.Equal(t, obj1.CreatedAt.UTC(), obj2.CreatedAt.UTC()) -} diff --git a/pkg/code/data/user/identity/postgres/store.go b/pkg/code/data/user/identity/postgres/store.go deleted file mode 100644 index fd2fef2b..00000000 --- a/pkg/code/data/user/identity/postgres/store.go +++ /dev/null @@ -1,49 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_identity "github.com/code-payments/code-server/pkg/code/data/user/identity" -) - -type store struct { - db *sqlx.DB -} - -// New returns a postgres backed user_identity.Store. -func New(db *sql.DB) user_identity.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Put implements user_identity.Store.Put -func (s *store) Put(ctx context.Context, record *user_identity.Record) error { - model, err := toModel(record) - if err != nil { - return err - } - return model.dbSave(ctx, s.db) -} - -// GetByID implements user_identity.Store.GetByID -func (s *store) GetByID(ctx context.Context, id *user.UserID) (*user_identity.Record, error) { - model, err := dbGetByID(ctx, s.db, id) - if err != nil { - return nil, err - } - return fromModel(model) -} - -// GetByView implements user_identity.Store.GetByView -func (s *store) GetByView(ctx context.Context, view *user.View) (*user_identity.Record, error) { - model, err := dbGetByView(ctx, s.db, view) - if err != nil { - return nil, err - } - return fromModel(model) -} diff --git a/pkg/code/data/user/identity/postgres/store_test.go b/pkg/code/data/user/identity/postgres/store_test.go deleted file mode 100644 index 75018363..00000000 --- a/pkg/code/data/user/identity/postgres/store_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - user_identity "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/code/data/user/identity/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_appuser( - id SERIAL NOT NULL PRIMARY KEY, - - user_id UUID NOT NULL, - phone_number TEXT NOT NULL, - is_staff_user BOOL NOT NULL, - is_banned BOOL NOT NULL, - created_at TIMESTAMP WITH TIME ZONE, - - CONSTRAINT codewallet__core_appuser__uniq__user_id UNIQUE (user_id), - CONSTRAINT codewallet__core_appuser__uniq__phone_number UNIQUE (phone_number) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_appuser; - ` -) - -var ( - testStore user_identity.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestUserIdentityPostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/user/identity/store.go b/pkg/code/data/user/identity/store.go deleted file mode 100644 index 76b9e9c2..00000000 --- a/pkg/code/data/user/identity/store.go +++ /dev/null @@ -1,60 +0,0 @@ -package identity - -import ( - "context" - "errors" - "time" - - "github.com/code-payments/code-server/pkg/code/data/user" -) - -var ( - // ErrNotFound is returned when a user being fetched does not exist. - ErrNotFound = errors.New("user not found") - - // ErrAlreadyExists is returned when a new user record is being added already - // exists. - ErrAlreadyExists = errors.New("user already exists") -) - -// User is the highest order of a form of identity. -type Record struct { - ID *user.UserID - View *user.View - IsStaffUser bool - IsBanned bool - CreatedAt time.Time -} - -// Store manages a user's identity. -type Store interface { - // Put creates a new user. - Put(ctx context.Context, record *Record) error - - // GetByID fetches a user by its ID. - GetByID(ctx context.Context, id *user.UserID) (*Record, error) - - // GetByView fetches a user by a view. - GetByView(ctx context.Context, view *user.View) (*Record, error) -} - -// Validate validates a Record -func (r *Record) Validate() error { - if r == nil { - return errors.New("record is nil") - } - - if err := r.ID.Validate(); err != nil { - return err - } - - if err := r.View.Validate(); err != nil { - return err - } - - if r.CreatedAt.IsZero() { - return errors.New("creation time is zero") - } - - return nil -} diff --git a/pkg/code/data/user/identity/tests/tests.go b/pkg/code/data/user/identity/tests/tests.go deleted file mode 100644 index 8ebeecac..00000000 --- a/pkg/code/data/user/identity/tests/tests.go +++ /dev/null @@ -1,70 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_identity "github.com/code-payments/code-server/pkg/code/data/user/identity" -) - -func RunTests(t *testing.T, s user_identity.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s user_identity.Store){ - testHappyPath, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s user_identity.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - phoneNumber := "+12223334444" - - record := &user_identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - IsStaffUser: true, - IsBanned: true, - CreatedAt: time.Now(), - } - - _, err := s.GetByID(ctx, record.ID) - assert.Equal(t, user_identity.ErrNotFound, err) - - _, err = s.GetByView(ctx, record.View) - assert.Equal(t, user_identity.ErrNotFound, err) - - require.NoError(t, s.Put(ctx, record)) - - actual, err := s.GetByID(ctx, record.ID) - require.NoError(t, err) - assertEqualUsers(t, record, actual) - - actual, err = s.GetByView(ctx, record.View) - require.NoError(t, err) - assertEqualUsers(t, record, actual) - - err = s.Put(ctx, record) - assert.Equal(t, user_identity.ErrAlreadyExists, err) - }) -} - -func assertEqualUsers(t *testing.T, obj1, obj2 *user_identity.Record) { - require.NoError(t, obj1.Validate()) - require.NoError(t, obj2.Validate()) - - assert.Equal(t, obj1.ID, obj2.ID) - assert.Equal(t, obj1.IsStaffUser, obj2.IsStaffUser) - assert.Equal(t, obj1.IsBanned, obj2.IsBanned) - assert.Equal(t, *obj1.View.PhoneNumber, *obj2.View.PhoneNumber) - assert.Equal(t, obj1.CreatedAt.UTC().Truncate(time.Second), obj2.CreatedAt.UTC().Truncate(time.Second)) -} diff --git a/pkg/code/data/user/model.go b/pkg/code/data/user/model.go deleted file mode 100644 index 96973642..00000000 --- a/pkg/code/data/user/model.go +++ /dev/null @@ -1,165 +0,0 @@ -package user - -import ( - "errors" - - "github.com/google/uuid" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - - "github.com/code-payments/code-server/pkg/phone" -) - -// UserID uniquely identifies a user -type UserID struct { - id uuid.UUID -} - -// DataContainerID uniquely identifies a container where a user can store a copy -// of their data -type DataContainerID struct { - id uuid.UUID -} - -// IdentifyingFeatures are a set of features that can be used to deterministaclly -// identify a user. -type IdentifyingFeatures struct { - PhoneNumber *string -} - -// View is a well-defined set of identifying features. It is contrained to having -// exactly one feature set. The semantics for overlapping views have not been -// defined, if they even make sense to begin with. -type View struct { - PhoneNumber *string -} - -// NewUserID returns a new random UserID -func NewUserID() *UserID { - return &UserID{ - id: uuid.New(), - } -} - -// GetUserIDFromProto returns a UserID from the protobuf message -func GetUserIDFromProto(proto *commonpb.UserId) (*UserID, error) { - id, err := uuid.FromBytes(proto.Value) - if err != nil { - return nil, err - } - return &UserID{id}, nil -} - -// GetUserIDFromString parses a UserID from a string value -func GetUserIDFromString(value string) (*UserID, error) { - id, err := uuid.Parse(value) - if err != nil { - return nil, err - } - - return &UserID{id}, nil -} - -// Validate validate a UserID -func (id *UserID) Validate() error { - if id == nil { - return errors.New("user id is nil") - } - - var defaultUUID uuid.UUID - if id.id == defaultUUID { - return errors.New("user id was not randomly generated") - } - - return nil -} - -// String returns the string form of a UserID -func (id *UserID) String() string { - return id.id.String() -} - -// Proto returns a UserID into its protobuf message form -func (id *UserID) Proto() *commonpb.UserId { - return &commonpb.UserId{ - Value: id.id[:], - } -} - -// NewDataContainerID returns a new random DataContainerID -func NewDataContainerID() *DataContainerID { - return &DataContainerID{ - id: uuid.New(), - } -} - -// GetDataContainerIDFromProto returns a UserID from the protobuf message -func GetDataContainerIDFromProto(proto *commonpb.DataContainerId) (*DataContainerID, error) { - id, err := uuid.FromBytes(proto.Value) - if err != nil { - return nil, err - } - return &DataContainerID{id}, nil -} - -// GetDataContainerIDFromString parses a DataContainerID from a string value -func GetDataContainerIDFromString(value string) (*DataContainerID, error) { - id, err := uuid.Parse(value) - if err != nil { - return nil, err - } - - return &DataContainerID{id}, nil -} - -// Validate validate a DataContainerID -func (id *DataContainerID) Validate() error { - if id == nil { - return errors.New("data container id is nil") - } - - var defaultUUID uuid.UUID - if id.id == defaultUUID { - return errors.New("data container id was not randomly generated") - } - - return nil -} - -// String returns the string form of a DataContainerID -func (id *DataContainerID) String() string { - return id.id.String() -} - -// Proto returns a UserID into its protobuf message form -func (id *DataContainerID) Proto() *commonpb.DataContainerId { - return &commonpb.DataContainerId{ - Value: id.id[:], - } -} - -// Validate validates an IdentifyingFeatures -func (f *IdentifyingFeatures) Validate() error { - if f.PhoneNumber == nil { - return errors.New("must specify at least one identifying feature") - } - - if !phone.IsE164Format(*f.PhoneNumber) { - return errors.New("phone number doesn't match E.164 standard") - } - - return nil -} - -// Validate validates a View -func (v *View) Validate() error { - if v.PhoneNumber == nil { - return errors.New("must specify exactly one identifying feature") - } - - if !phone.IsE164Format(*v.PhoneNumber) { - return errors.New("phone number doesn't match E.164 standard") - } - - return nil -} diff --git a/pkg/code/data/user/model_test.go b/pkg/code/data/user/model_test.go deleted file mode 100644 index bf758549..00000000 --- a/pkg/code/data/user/model_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package user - -import ( - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" -) - -func TestUserIDStringConverstion(t *testing.T) { - stringValue := uuid.New().String() - userID, err := GetUserIDFromString(stringValue) - require.NoError(t, err) - assert.Equal(t, stringValue, userID.String()) -} - -func TestUserIDProtoConverstion(t *testing.T) { - uuid := uuid.New() - protoValue := &commonpb.UserId{ - Value: uuid[:], - } - userID, err := GetUserIDFromProto(protoValue) - require.NoError(t, err) - assert.Equal(t, protoValue, userID.Proto()) -} - -func TestUserIDValidation(t *testing.T) { - var nilUserID *UserID - emptyUserID := &UserID{} - assert.Error(t, nilUserID.Validate()) - assert.Error(t, emptyUserID.Validate()) - assert.NoError(t, NewUserID().Validate()) -} - -func TestDataContainerIDStringConverstion(t *testing.T) { - uuid := uuid.New() - dataContainerID, err := GetDataContainerIDFromString(uuid.String()) - require.NoError(t, err) - assert.Equal(t, uuid.String(), dataContainerID.String()) -} - -func TestDataContainerIDProtoConverstion(t *testing.T) { - uuid := uuid.New() - protoValue := &commonpb.DataContainerId{ - Value: uuid[:], - } - dataContainerID, err := GetDataContainerIDFromProto(protoValue) - require.NoError(t, err) - assert.Equal(t, protoValue, dataContainerID.Proto()) -} - -func TestDataContainerIDValidation(t *testing.T) { - var nilDataContainerID *DataContainerID - emptyDataContainerID := &DataContainerID{} - assert.Error(t, nilDataContainerID.Validate()) - assert.Error(t, emptyDataContainerID.Validate()) - assert.NoError(t, NewDataContainerID().Validate()) -} diff --git a/pkg/code/data/user/storage/memory/store.go b/pkg/code/data/user/storage/memory/store.go deleted file mode 100644 index 8390a6bb..00000000 --- a/pkg/code/data/user/storage/memory/store.go +++ /dev/null @@ -1,102 +0,0 @@ -package memory - -import ( - "context" - "fmt" - "sync" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_storage "github.com/code-payments/code-server/pkg/code/data/user/storage" -) - -type store struct { - mu sync.RWMutex - dataContainersByID map[string]*user_storage.Record - dataContainersByFeatures map[string]*user_storage.Record -} - -// New returns an in memory user_storage.Store -func New() user_storage.Store { - return &store{ - dataContainersByID: make(map[string]*user_storage.Record), - dataContainersByFeatures: make(map[string]*user_storage.Record), - } -} - -// Put implements user_storage.Store.Put -func (s *store) Put(ctx context.Context, container *user_storage.Record) error { - if err := container.Validate(); err != nil { - return err - } - - s.mu.Lock() - defer s.mu.Unlock() - - _, ok := s.dataContainersByID[container.ID.String()] - if ok { - return user_storage.ErrAlreadyExists - } - - key := getFeaturesKey(container.OwnerAccount, container.IdentifyingFeatures) - _, ok = s.dataContainersByFeatures[key] - if ok { - return user_storage.ErrAlreadyExists - } - - copy := &user_storage.Record{ - ID: container.ID, - OwnerAccount: container.OwnerAccount, - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: container.IdentifyingFeatures.PhoneNumber, - }, - CreatedAt: container.CreatedAt, - } - - s.dataContainersByID[container.ID.String()] = copy - s.dataContainersByFeatures[key] = copy - - return nil -} - -// GetByID implements user_storage.Store.GetByID -func (s *store) GetByID(ctx context.Context, id *user.DataContainerID) (*user_storage.Record, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - record, ok := s.dataContainersByID[id.String()] - if !ok { - return nil, user_storage.ErrNotFound - } - - return record, nil -} - -// GetByFeatures implements user_storage.Store.GetByFeatures -func (s *store) GetByFeatures(ctx context.Context, ownerAccount string, features *user.IdentifyingFeatures) (*user_storage.Record, error) { - if err := features.Validate(); err != nil { - return nil, err - } - - s.mu.RLock() - defer s.mu.RUnlock() - - key := getFeaturesKey(ownerAccount, features) - record, ok := s.dataContainersByFeatures[key] - if !ok { - return nil, user_storage.ErrNotFound - } - - return record, nil -} - -func getFeaturesKey(ownerAccount string, features *user.IdentifyingFeatures) string { - return fmt.Sprintf("%s:%s", ownerAccount, *features.PhoneNumber) -} - -func (s *store) reset() { - s.mu.Lock() - defer s.mu.Unlock() - - s.dataContainersByID = make(map[string]*user_storage.Record) - s.dataContainersByFeatures = make(map[string]*user_storage.Record) -} diff --git a/pkg/code/data/user/storage/memory/store_test.go b/pkg/code/data/user/storage/memory/store_test.go deleted file mode 100644 index af1121c2..00000000 --- a/pkg/code/data/user/storage/memory/store_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package memory - -import ( - "testing" - - "github.com/code-payments/code-server/pkg/code/data/user/storage/tests" -) - -func TestUserStorageMemoryStore(t *testing.T) { - testStore := New() - teardown := func() { - testStore.(*store).reset() - } - tests.RunTests(t, testStore, teardown) -} diff --git a/pkg/code/data/user/storage/postgres/model.go b/pkg/code/data/user/storage/postgres/model.go deleted file mode 100644 index 72545b66..00000000 --- a/pkg/code/data/user/storage/postgres/model.go +++ /dev/null @@ -1,101 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - "time" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_storage "github.com/code-payments/code-server/pkg/code/data/user/storage" - - pgutil "github.com/code-payments/code-server/pkg/database/postgres" -) - -const ( - tableName = "codewallet__core_appuserstorage" -) - -type model struct { - ID sql.NullInt64 `db:"id"` - ContainerID string `db:"container_id"` - OwnerAccount string `db:"owner_account"` - PhoneNumber string `db:"phone_number"` - CreatedAt time.Time `db:"created_at"` -} - -func toModel(obj *user_storage.Record) (*model, error) { - if err := obj.Validate(); err != nil { - return nil, err - } - - return &model{ - ContainerID: obj.ID.String(), - OwnerAccount: obj.OwnerAccount, - PhoneNumber: *obj.IdentifyingFeatures.PhoneNumber, - CreatedAt: obj.CreatedAt.UTC(), - }, nil -} - -func fromModel(obj *model) (*user_storage.Record, error) { - dataContainerID, err := user.GetDataContainerIDFromString(obj.ContainerID) - if err != nil { - return nil, errors.Wrap(err, "error parsing data container id") - } - - return &user_storage.Record{ - ID: dataContainerID, - OwnerAccount: obj.OwnerAccount, - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &obj.PhoneNumber, - }, - CreatedAt: obj.CreatedAt.UTC(), - }, nil -} - -func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error { - query := `INSERT INTO ` + tableName + ` - (container_id, owner_account, phone_number, created_at) - VALUES ($1, $2, $3, $4) - RETURNING id, container_id, owner_account, phone_number, created_at` - - err := db.QueryRowxContext( - ctx, - query, - m.ContainerID, - m.OwnerAccount, - m.PhoneNumber, - m.CreatedAt.UTC(), - ).StructScan(m) - return pgutil.CheckUniqueViolation(err, user_storage.ErrAlreadyExists) -} - -func dbGetByID(ctx context.Context, db *sqlx.DB, id *user.DataContainerID) (*model, error) { - query := `SELECT id, container_id, owner_account, phone_number, created_at FROM ` + tableName + ` - WHERE container_id = $1` - - result := &model{} - err := db.GetContext(ctx, result, query, id.String()) - if err != nil { - return nil, pgutil.CheckNoRows(err, user_storage.ErrNotFound) - } - return result, nil -} - -func dbGetByFeatures(ctx context.Context, db *sqlx.DB, ownerAccount string, features *user.IdentifyingFeatures) (*model, error) { - if err := features.Validate(); err != nil { - return nil, err - } - - query := `SELECT id, container_id, owner_account, phone_number, created_at FROM ` + tableName + ` - WHERE owner_account = $1 AND phone_number = $2` - - result := &model{} - err := db.GetContext(ctx, result, query, ownerAccount, *features.PhoneNumber) - if err != nil { - return nil, pgutil.CheckNoRows(err, user_storage.ErrNotFound) - } - return result, nil -} diff --git a/pkg/code/data/user/storage/postgres/model_test.go b/pkg/code/data/user/storage/postgres/model_test.go deleted file mode 100644 index 1caf86f3..00000000 --- a/pkg/code/data/user/storage/postgres/model_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package postgres - -import ( - "crypto/ed25519" - "testing" - "time" - - "github.com/mr-tron/base58/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_storage "github.com/code-payments/code-server/pkg/code/data/user/storage" -) - -func TestModelConversion(t *testing.T) { - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - phoneNumber := "+12223334444" - dataContainer := &user_storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: base58.Encode(pub), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - - model, err := toModel(dataContainer) - require.NoError(t, err) - - actual, err := fromModel(model) - require.NoError(t, err) - - assertEqualDataContainers(t, dataContainer, actual) -} - -func assertEqualDataContainers(t *testing.T, obj1, obj2 *user_storage.Record) { - require.NoError(t, obj1.Validate()) - require.NoError(t, obj2.Validate()) - - assert.Equal(t, obj1.ID, obj2.ID) - assert.Equal(t, obj1.OwnerAccount, obj2.OwnerAccount) - assert.Equal(t, *obj1.IdentifyingFeatures.PhoneNumber, *obj2.IdentifyingFeatures.PhoneNumber) - assert.Equal(t, obj1.CreatedAt.UTC(), obj2.CreatedAt.UTC()) -} diff --git a/pkg/code/data/user/storage/postgres/store.go b/pkg/code/data/user/storage/postgres/store.go deleted file mode 100644 index 32c72bbd..00000000 --- a/pkg/code/data/user/storage/postgres/store.go +++ /dev/null @@ -1,49 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "github.com/jmoiron/sqlx" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_storage "github.com/code-payments/code-server/pkg/code/data/user/storage" -) - -type store struct { - db *sqlx.DB -} - -// New returns a postgres backed user_storage.Store. -func New(db *sql.DB) user_storage.Store { - return &store{ - db: sqlx.NewDb(db, "pgx"), - } -} - -// Put implements user_storage.Store.Put -func (s *store) Put(ctx context.Context, container *user_storage.Record) error { - model, err := toModel(container) - if err != nil { - return err - } - return model.dbSave(ctx, s.db) -} - -// GetByID implements user_storage.Store.GetByID -func (s *store) GetByID(ctx context.Context, id *user.DataContainerID) (*user_storage.Record, error) { - model, err := dbGetByID(ctx, s.db, id) - if err != nil { - return nil, err - } - return fromModel(model) -} - -// GeByFeatures implements user_storage.Store.GetByFeatures -func (s *store) GetByFeatures(ctx context.Context, ownerAccount string, features *user.IdentifyingFeatures) (*user_storage.Record, error) { - model, err := dbGetByFeatures(ctx, s.db, ownerAccount, features) - if err != nil { - return nil, err - } - return fromModel(model) -} diff --git a/pkg/code/data/user/storage/postgres/store_test.go b/pkg/code/data/user/storage/postgres/store_test.go deleted file mode 100644 index 48709465..00000000 --- a/pkg/code/data/user/storage/postgres/store_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package postgres - -import ( - "database/sql" - "os" - "testing" - - "github.com/ory/dockertest/v3" - "github.com/sirupsen/logrus" - - user_storage "github.com/code-payments/code-server/pkg/code/data/user/storage" - "github.com/code-payments/code-server/pkg/code/data/user/storage/tests" - - postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" - - _ "github.com/jackc/pgx/v4/stdlib" -) - -const ( - // Used for testing ONLY, the table and migrations are external to this repository - tableCreate = ` - CREATE TABLE codewallet__core_appuserstorage( - id SERIAL NOT NULL PRIMARY KEY, - - container_id UUID NOT NULL, - owner_account TEXT NOT NULL, - phone_number TEXT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE, - - CONSTRAINT codewallet__core_appuserstorage__uniq__container_id UNIQUE (container_id), - CONSTRAINT codewallet__core_appuserstorage__uniq__owner_account__and__phone_number UNIQUE (owner_account, phone_number) - ); - ` - - // Used for testing ONLY, the table and migrations are external to this repository - tableDestroy = ` - DROP TABLE codewallet__core_appuserstorage; - ` -) - -var ( - testStore user_storage.Store - teardown func() -) - -func TestMain(m *testing.M) { - log := logrus.StandardLogger() - - testPool, err := dockertest.NewPool("") - if err != nil { - log.WithError(err).Error("Error creating docker pool") - os.Exit(1) - } - - var cleanUpFunc func() - db, cleanUpFunc, err := postgrestest.StartPostgresDB(testPool) - if err != nil { - log.WithError(err).Error("Error starting postgres image") - os.Exit(1) - } - defer db.Close() - - if err := createTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error creating test tables") - cleanUpFunc() - os.Exit(1) - } - - testStore = New(db) - teardown = func() { - if pc := recover(); pc != nil { - cleanUpFunc() - panic(pc) - } - - if err := resetTestTables(db); err != nil { - logrus.StandardLogger().WithError(err).Error("Error resetting test tables") - cleanUpFunc() - os.Exit(1) - } - } - - code := m.Run() - cleanUpFunc() - os.Exit(code) -} - -func TestUserStoragePostgresStore(t *testing.T) { - tests.RunTests(t, testStore, teardown) -} - -func createTestTables(db *sql.DB) error { - _, err := db.Exec(tableCreate) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not create test tables") - return err - } - return nil -} - -func resetTestTables(db *sql.DB) error { - _, err := db.Exec(tableDestroy) - if err != nil { - logrus.StandardLogger().WithError(err).Error("could not drop test tables") - return err - } - - return createTestTables(db) -} diff --git a/pkg/code/data/user/storage/store.go b/pkg/code/data/user/storage/store.go deleted file mode 100644 index 2421d264..00000000 --- a/pkg/code/data/user/storage/store.go +++ /dev/null @@ -1,68 +0,0 @@ -package storage - -import ( - "context" - "errors" - "time" - - "github.com/code-payments/code-server/pkg/code/data/user" -) - -var ( - // ErrNotFound is returned when a data container being fetched does not exist. - ErrNotFound = errors.New("data container not found") - - // ErrAlreadyExists is returned when a new container being added already - // exists. - ErrAlreadyExists = errors.New("data container already exists") -) - -// DataContainer represents a logical boundary that separates and isolates a copy -// of a user's data. The owner account acts as the owner of the copy of data as -// both a form of authentication (via signing of the owner account private key) -// and authorization (by exact match of an authenticated owner account address -// linked to a container). The ID must be used as part of the key for user data -// systems. -type Record struct { - ID *user.DataContainerID - OwnerAccount string - IdentifyingFeatures *user.IdentifyingFeatures - CreatedAt time.Time -} - -// Store manages copies of a user's data using logical containers. -type Store interface { - // Put creates a new data container. - Put(ctx context.Context, record *Record) error - - // GetByID gets a data container by its ID. - GetByID(ctx context.Context, id *user.DataContainerID) (*Record, error) - - // GetByFeatures gets a data container that matches the set of features. - GetByFeatures(ctx context.Context, ownerAccount string, features *user.IdentifyingFeatures) (*Record, error) -} - -// Validate validates a Record -func (r *Record) Validate() error { - if r == nil { - return errors.New("record is nil") - } - - if err := r.ID.Validate(); err != nil { - return err - } - - if len(r.OwnerAccount) == 0 { - return errors.New("owner account is empty") - } - - if err := r.IdentifyingFeatures.Validate(); err != nil { - return err - } - - if r.CreatedAt.IsZero() { - return errors.New("creation time is zero") - } - - return nil -} diff --git a/pkg/code/data/user/storage/tests/tests.go b/pkg/code/data/user/storage/tests/tests.go deleted file mode 100644 index 135540f6..00000000 --- a/pkg/code/data/user/storage/tests/tests.go +++ /dev/null @@ -1,73 +0,0 @@ -package tests - -import ( - "context" - "crypto/ed25519" - "testing" - "time" - - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/code-payments/code-server/pkg/code/data/user" - user_storage "github.com/code-payments/code-server/pkg/code/data/user/storage" -) - -func RunTests(t *testing.T, s user_storage.Store, teardown func()) { - for _, tf := range []func(t *testing.T, s user_storage.Store){ - testHappyPath, - } { - tf(t, s) - teardown() - } -} - -func testHappyPath(t *testing.T, s user_storage.Store) { - t.Run("testHappyPath", func(t *testing.T) { - ctx := context.Background() - - pub, _, err := ed25519.GenerateKey(nil) - require.NoError(t, err) - - phoneNumber := "+12223334444" - - record := &user_storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: base58.Encode(pub), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - - _, err = s.GetByID(ctx, record.ID) - assert.Equal(t, user_storage.ErrNotFound, err) - - _, err = s.GetByFeatures(ctx, record.OwnerAccount, record.IdentifyingFeatures) - assert.Equal(t, user_storage.ErrNotFound, err) - - require.NoError(t, s.Put(ctx, record)) - - actual, err := s.GetByID(ctx, record.ID) - require.NoError(t, err) - assertEqualDataContainers(t, record, actual) - - actual, err = s.GetByFeatures(ctx, record.OwnerAccount, record.IdentifyingFeatures) - require.NoError(t, err) - assertEqualDataContainers(t, record, actual) - - err = s.Put(ctx, record) - assert.Equal(t, user_storage.ErrAlreadyExists, err) - }) -} - -func assertEqualDataContainers(t *testing.T, obj1, obj2 *user_storage.Record) { - require.NoError(t, obj1.Validate()) - require.NoError(t, obj2.Validate()) - - assert.Equal(t, obj1.ID, obj2.ID) - assert.Equal(t, obj1.OwnerAccount, obj2.OwnerAccount) - assert.Equal(t, *obj1.IdentifyingFeatures.PhoneNumber, *obj2.IdentifyingFeatures.PhoneNumber) - assert.Equal(t, obj1.CreatedAt.UTC().Truncate(time.Second), obj2.CreatedAt.UTC().Truncate(time.Second)) -} diff --git a/pkg/code/event/client.go b/pkg/code/event/client.go deleted file mode 100644 index df3a7fb9..00000000 --- a/pkg/code/event/client.go +++ /dev/null @@ -1,52 +0,0 @@ -package event - -import ( - "context" - "net" - - "github.com/oschwald/maxminddb-golang" - - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/netutil" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/event" -) - -// InjectClientDetails injects client details into the provided event record. Metadata -// is provided on a best-effort basis. -// -// todo: We probably need a bit of a refactor for MaxMind (ie. put behind an interface, add to DataProvider, etc) -func InjectClientDetails(ctx context.Context, db *maxminddb.Reader, eventRecord *event.Record, isSource bool) { - ip, err := client.GetIPAddr(ctx) - if err != nil { - return - } - - if isSource { - eventRecord.SourceClientIp = pointer.String(ip) - } else { - eventRecord.DestinationClientIp = pointer.String(ip) - } - - if db == nil { - return - } - - parsed := net.ParseIP(ip) - if parsed == nil { - return - } - - metadata, err := netutil.GetIpMetadata(ctx, db, ip) - if err != nil { - return - } - - if isSource { - eventRecord.SourceClientCity = metadata.City - eventRecord.SourceClientCountry = metadata.Country - } else { - eventRecord.DestinationClientCity = metadata.City - eventRecord.DestinationClientCountry = metadata.Country - } -} diff --git a/pkg/code/exchangerate/validation.go b/pkg/code/exchangerate/validation.go index becd056c..8385bec8 100644 --- a/pkg/code/exchangerate/validation.go +++ b/pkg/code/exchangerate/validation.go @@ -11,15 +11,19 @@ import ( transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/kin" + "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/currency" + currency_lib "github.com/code-payments/code-server/pkg/currency" + "github.com/code-payments/code-server/pkg/database/query" ) // todo: add tests, but generally well tested in server tests since that's where most of this originated +var ( + SmallestSendAmount = common.CoreMintQuarksPerUnit / 100 +) + // GetPotentialClientExchangeRates gets a set of exchange rates that a client // attempting to maintain a latest state could have fetched from the currency // server. @@ -63,11 +67,10 @@ func GetPotentialClientExchangeRates(ctx context.Context, data code_data.Provide func ValidateClientExchangeData(ctx context.Context, data code_data.Provider, proto *transactionpb.ExchangeData) (bool, string, error) { currencyCode := strings.ToLower(proto.Currency) switch currencyCode { - case string(currency_lib.KIN): + case string(common.CoreMintSymbol): if proto.ExchangeRate != 1.0 { - return false, "kin exchange rate must be 1", nil + return false, "core mint exchange rate must be 1", nil } - default: // Validate the exchange rate with what Code would have returned exchangeRecords, err := GetPotentialClientExchangeRates(ctx, data, currency_lib.Code(currencyCode)) @@ -100,9 +103,8 @@ func ValidateClientExchangeData(ctx context.Context, data code_data.Provider, pr // // todo: This uses string conversions, which is less than ideal, but the only // thing available at the time of writing this for conversion. - smallestSendAmount := float64(kin.ToQuarks(1)) - quarksFromCurrency, _ := kin.StrToQuarks(fmt.Sprintf("%.5f", proto.NativeAmount/proto.ExchangeRate)) - if math.Abs(float64(quarksFromCurrency-int64(proto.Quarks))) > smallestSendAmount+100 { + quarksFromCurrency, _ := common.StrToQuarks(fmt.Sprintf("%.6f", proto.NativeAmount/proto.ExchangeRate)) + if math.Abs(float64(quarksFromCurrency-int64(proto.Quarks))) > float64(SmallestSendAmount) { return false, "payment native amount and quark value mismatch", nil } diff --git a/pkg/code/lawenforcement/anti_money_laundering.go b/pkg/code/lawenforcement/anti_money_laundering.go index 3e4bec66..e71170e7 100644 --- a/pkg/code/lawenforcement/anti_money_laundering.go +++ b/pkg/code/lawenforcement/anti_money_laundering.go @@ -2,18 +2,12 @@ package lawenforcement import ( "context" - "time" - "github.com/pkg/errors" "github.com/sirupsen/logrus" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/code/balance" - "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/metrics" ) const ( @@ -46,93 +40,97 @@ func (g *AntiMoneyLaunderingGuard) AllowMoneyMovement(ctx context.Context, inten tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowMoneyMovement") defer tracer.End() - var usdMarketValue float64 - var consumptionCalculator func(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) - switch intentRecord.IntentType { - case intent.SendPublicPayment, intent.ReceivePaymentsPublicly: - // Public movements of money are not subject to AML rules. They are - // done in the open. - return true, nil - case intent.SendPrivatePayment: - usdMarketValue = intentRecord.SendPrivatePaymentMetadata.UsdMarketValue - consumptionCalculator = g.data.GetTransactedAmountForAntiMoneyLaundering - case intent.ReceivePaymentsPrivately: - // Allow users to always receive in-app payments from their temporary incoming - // accounts. The payment was already allowed when initiatied on the send side. - if !intentRecord.ReceivePaymentsPrivatelyMetadata.IsDeposit { + /* + var usdMarketValue float64 + var consumptionCalculator func(ctx context.Context, phoneNumber string, since time.Time) (uint64, float64, error) + switch intentRecord.IntentType { + case intent.SendPublicPayment, intent.ReceivePaymentsPublicly: + // Public movements of money are not subject to AML rules. They are + // done in the open. return true, nil - } - - owner, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { + case intent.SendPrivatePayment: + usdMarketValue = intentRecord.SendPrivatePaymentMetadata.UsdMarketValue + consumptionCalculator = g.data.GetTransactedAmountForAntiMoneyLaundering + case intent.ReceivePaymentsPrivately: + // Allow users to always receive in-app payments from their temporary incoming + // accounts. The payment was already allowed when initiatied on the send side. + if !intentRecord.ReceivePaymentsPrivatelyMetadata.IsDeposit { + return true, nil + } + + owner, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) + if err != nil { + tracer.OnError(err) + return false, err + } + + totalPrivateBalance, err := balance.GetPrivateBalance(ctx, g.data, owner) + if err != nil { + tracer.OnError(err) + return false, err + } + + usdExchangeRecord, err := g.data.GetExchangeRate(ctx, currency_lib.USD, time.Now()) + if err != nil { + tracer.OnError(err) + return false, err + } + + // Do they need the deposit based on total private balance? Note: clients + // always try to deposit the max as a mechanism of hiding in the crowd, so + // we can only consider the current balance and whether it makes sense. + if usdExchangeRecord.Rate*float64(kin.FromQuarks(totalPrivateBalance)) >= maxUsdPrivateBalance { + recordDenialEvent(ctx, "private balance exceeds threshold") + return false, nil + } + + // Otherwise, limit deposits in line with expectations for payments. + usdMarketValue = intentRecord.ReceivePaymentsPrivatelyMetadata.UsdMarketValue + consumptionCalculator = g.data.GetDepositedAmountForAntiMoneyLaundering + default: + err := errors.New("intent record must be a send or receive payment") tracer.OnError(err) return false, err } - totalPrivateBalance, err := balance.GetPrivateBalance(ctx, g.data, owner) - if err != nil { + if intentRecord.InitiatorPhoneNumber == nil { + err := errors.New("anti-money laundering guard requires an identity") tracer.OnError(err) return false, err } - usdExchangeRecord, err := g.data.GetExchangeRate(ctx, currency_lib.USD, time.Now()) + log := g.log.WithFields(logrus.Fields{ + "method": "AllowMoneyMovement", + "owner": intentRecord.InitiatorOwnerAccount, + "phone_number": *intentRecord.InitiatorPhoneNumber, + "usd_value": usdMarketValue, + }) + + phoneNumber := *intentRecord.InitiatorPhoneNumber + + // Bound the maximum dollar value of a payment + if usdMarketValue > maxUsdTransactionValue { + log.Info("denying intent that exceeds per-transaction usd value") + recordDenialEvent(ctx, "exceeds per-transaction usd value") + return false, nil + } + + // Bound the maximum dollar value of payments in the last day + _, usdInLastDay, err := consumptionCalculator(ctx, phoneNumber, time.Now().Add(-24*time.Hour)) if err != nil { + log.WithError(err).Warn("failure calculating previous day transaction amount") tracer.OnError(err) return false, err } - // Do they need the deposit based on total private balance? Note: clients - // always try to deposit the max as a mechanism of hiding in the crowd, so - // we can only consider the current balance and whether it makes sense. - if usdExchangeRecord.Rate*float64(kin.FromQuarks(totalPrivateBalance)) >= maxUsdPrivateBalance { - recordDenialEvent(ctx, "private balance exceeds threshold") + if usdInLastDay+usdMarketValue > maxDailyUsdLimit { + log.Info("denying intent that exceeds daily usd limit") + recordDenialEvent(ctx, "exceeds daily usd value") return false, nil } - // Otherwise, limit deposits in line with expectations for payments. - usdMarketValue = intentRecord.ReceivePaymentsPrivatelyMetadata.UsdMarketValue - consumptionCalculator = g.data.GetDepositedAmountForAntiMoneyLaundering - default: - err := errors.New("intent record must be a send or receive payment") - tracer.OnError(err) - return false, err - } - - if intentRecord.InitiatorPhoneNumber == nil { - err := errors.New("anti-money laundering guard requires an identity") - tracer.OnError(err) - return false, err - } - - log := g.log.WithFields(logrus.Fields{ - "method": "AllowMoneyMovement", - "owner": intentRecord.InitiatorOwnerAccount, - "phone_number": *intentRecord.InitiatorPhoneNumber, - "usd_value": usdMarketValue, - }) - - phoneNumber := *intentRecord.InitiatorPhoneNumber - - // Bound the maximum dollar value of a payment - if usdMarketValue > maxUsdTransactionValue { - log.Info("denying intent that exceeds per-transaction usd value") - recordDenialEvent(ctx, "exceeds per-transaction usd value") - return false, nil - } - - // Bound the maximum dollar value of payments in the last day - _, usdInLastDay, err := consumptionCalculator(ctx, phoneNumber, time.Now().Add(-24*time.Hour)) - if err != nil { - log.WithError(err).Warn("failure calculating previous day transaction amount") - tracer.OnError(err) - return false, err - } - - if usdInLastDay+usdMarketValue > maxDailyUsdLimit { - log.Info("denying intent that exceeds daily usd limit") - recordDenialEvent(ctx, "exceeds daily usd value") - return false, nil - } + return true, nil + */ return true, nil } diff --git a/pkg/code/lawenforcement/anti_money_laundering_test.go b/pkg/code/lawenforcement/anti_money_laundering_test.go index 918722fd..5e434d2c 100644 --- a/pkg/code/lawenforcement/anti_money_laundering_test.go +++ b/pkg/code/lawenforcement/anti_money_laundering_test.go @@ -1,5 +1,6 @@ package lawenforcement +/* import ( "context" "fmt" @@ -484,3 +485,4 @@ func setupPrivateBalance(t *testing.T, env amlTestEnv, owner *common.Account, ba require.NoError(t, env.data.PutAllActions(env.ctx, actionRecord)) } } +*/ diff --git a/pkg/code/limit/limits.go b/pkg/code/limit/limits.go index 077e9b0a..7b521c23 100644 --- a/pkg/code/limit/limits.go +++ b/pkg/code/limit/limits.go @@ -28,336 +28,337 @@ func init() { // todo: Better way of managing all of this var ( SendLimits = map[currency_lib.Code]SendLimit{ - "aed": {PerTransaction: 1_000.00, Daily: 3_500.00}, - "afn": {PerTransaction: 20_000.00, Daily: 85_000.00}, - "all": {PerTransaction: 25_000.00, Daily: 100_000.00}, - "amd": {PerTransaction: 100_000.00, Daily: 400_000.00}, - "ang": {PerTransaction: 500.00, Daily: 1_750.00}, - "aoa": {PerTransaction: 100_000.00, Daily: 500_000.00}, - "ars": {PerTransaction: 25_000.00, Daily: 125_000.00}, - "aud": {PerTransaction: 250.00, Daily: 1_250.00}, - "awg": {PerTransaction: 500.00, Daily: 1_500.00}, - "azn": {PerTransaction: 250.00, Daily: 1_500.00}, - "bam": {PerTransaction: 500.00, Daily: 1_500.00}, - "bbd": {PerTransaction: 500.00, Daily: 2_000.00}, - "bdt": {PerTransaction: 25_000.00, Daily: 90_000.00}, - "bgn": {PerTransaction: 500.00, Daily: 1_750.00}, - "bhd": {PerTransaction: 100.00, Daily: 350.00}, - "bif": {PerTransaction: 500_000.00, Daily: 2_000_000.00}, - "bmd": {PerTransaction: 250.00, Daily: 1_000.00}, - "bnd": {PerTransaction: 250.00, Daily: 1_250.00}, - "bob": {PerTransaction: 1_500.00, Daily: 6_500.00}, - "brl": {PerTransaction: 1_000.00, Daily: 5_000.00}, - "bsd": {PerTransaction: 250.00, Daily: 1_000.00}, - "btn": {PerTransaction: 20_000.00, Daily: 75_000.00}, - "bwp": {PerTransaction: 2_500.00, Daily: 10_000.00}, - "byn": {PerTransaction: 500.00, Daily: 2_500.00}, - "byr": {PerTransaction: 5_000_000.00, Daily: 17_500_000.00}, - "bzd": {PerTransaction: 500.00, Daily: 2_000.00}, - "cad": {PerTransaction: 250.00, Daily: 1_250.00}, - "cdf": {PerTransaction: 500_000.00, Daily: 2_000_000.00}, - "chf": {PerTransaction: 250.00, Daily: 900.00}, - "clf": {PerTransaction: 8.00, Daily: 30.00}, - "clp": {PerTransaction: 150_000.00, Daily: 800_000.00}, - "cny": {PerTransaction: 1_500.00, Daily: 6_500.00}, - "cop": {PerTransaction: 1_000_000.00, Daily: 4_000_000.00}, - "crc": {PerTransaction: 150_000.00, Daily: 60_0000.00}, - "cuc": {PerTransaction: 250.00, Daily: 1_000.00}, - "cup": {PerTransaction: 5_000.00, Daily: 25_000.00}, - "cve": {PerTransaction: 25_000.00, Daily: 100_000.00}, - "czk": {PerTransaction: 5_000.00, Daily: 22_500.00}, - "djf": {PerTransaction: 40_000.00, Daily: 175_000.00}, - "dkk": {PerTransaction: 1_000.00, Daily: 7_000.00}, - "dop": {PerTransaction: 12_500.00, Daily: 50_000.00}, - "dzd": {PerTransaction: 25_000.00, Daily: 125_000.00}, - "egp": {PerTransaction: 2_500.00, Daily: 17_500.00}, - "ern": {PerTransaction: 2_500.00, Daily: 15_000.00}, - "etb": {PerTransaction: 12_500.00, Daily: 50_000.00}, - "eur": {PerTransaction: 250.00, Daily: 999.99}, - "fjd": {PerTransaction: 500.00, Daily: 2_000.00}, - "fkp": {PerTransaction: 250.00, Daily: 800.00}, - "gbp": {PerTransaction: 250.00, Daily: 800.00}, - "gel": {PerTransaction: 500.00, Daily: 2_500.00}, - "ggp": {PerTransaction: 250.00, Daily: 800.00}, - "ghs": {PerTransaction: 2_250.00, Daily: 12_500.00}, - "gip": {PerTransaction: 250.00, Daily: 800.00}, - "gmd": {PerTransaction: 15_000.00, Daily: 50_000.00}, - "gnf": {PerTransaction: 2_000_000.00, Daily: 8_500_000.00}, - "gtq": {PerTransaction: 1_500.00, Daily: 7_500.00}, - "gyd": {PerTransaction: 50_000.00, Daily: 200_000.00}, - "hkd": {PerTransaction: 1_000.00, Daily: 7_500.00}, - "hnl": {PerTransaction: 5_000.00, Daily: 22_500.00}, - "hrk": {PerTransaction: 1_500.00, Daily: 7_250.00}, - "htg": {PerTransaction: 25_000.00, Daily: 125_000.00}, - "huf": {PerTransaction: 50_000.00, Daily: 375_000.00}, - "idr": {PerTransaction: 1_500_000.00, Daily: 14_000_000.00}, - "ils": {PerTransaction: 750.00, Daily: 3_000.00}, - "imp": {PerTransaction: 250.00, Daily: 800.00}, - "inr": {PerTransaction: 10_000.00, Daily: 80_000.00}, - "iqd": {PerTransaction: 250_000.00, Daily: 1_250_000.00}, - "irr": {PerTransaction: 10_000_000.00, Daily: 40_000_000.00}, - "isk": {PerTransaction: 25_000.00, Daily: 125_000.00}, - "jep": {PerTransaction: 250.00, Daily: 800.00}, - "jmd": {PerTransaction: 25_000.00, Daily: 150_000.00}, - "jod": {PerTransaction: 100.00, Daily: 700.00}, - "jpy": {PerTransaction: 25_000.00, Daily: 125_000.00}, - "kes": {PerTransaction: 25_000.00, Daily: 100_000.00}, - "kgs": {PerTransaction: 10_000.00, Daily: 80_000.00}, - "khr": {PerTransaction: 1_000_000.00, Daily: 4_000_000.00}, - "kmf": {PerTransaction: 100_000.00, Daily: 400_000.00}, - "kpw": {PerTransaction: 250_000.00, Daily: 900_000.00}, - "krw": {PerTransaction: 250_000.00, Daily: 1_250_000.00}, - "kwd": {PerTransaction: 50.00, Daily: 300.00}, - "kyd": {PerTransaction: 250.00, Daily: 800.00}, - "kzt": {PerTransaction: 100_000.00, Daily: 400_000.00}, - "lak": {PerTransaction: 2_500_000.00, Daily: 15_000_000.00}, - "lbp": {PerTransaction: 3_000_000.00, Daily: 15_000_000.00}, - "lkr": {PerTransaction: 50_000.00, Daily: 350_000.00}, - "lrd": {PerTransaction: 25_000.00, Daily: 150_000.00}, - "lsl": {PerTransaction: 2_500.00, Daily: 15_000.00}, - "ltl": {PerTransaction: 1_000.00, Daily: 2_500.00}, - "lvl": {PerTransaction: 150.00, Daily: 600.00}, - "lyd": {PerTransaction: 1_250.00, Daily: 4_500.00}, - "mad": {PerTransaction: 2_500.00, Daily: 10_000.00}, - "mdl": {PerTransaction: 2_500.00, Daily: 17_500.00}, - "mga": {PerTransaction: 1_000_000.00, Daily: 4_000_000.00}, - "mkd": {PerTransaction: 15_000.00, Daily: 50_000.00}, - "mmk": {PerTransaction: 500_000.00, Daily: 2_000_000.00}, - "mnt": {PerTransaction: 750_000.00, Daily: 3_000_000.00}, - "mop": {PerTransaction: 2_000.00, Daily: 8_000.00}, - "mru": {PerTransaction: 10000.00, Daily: 35000.00}, - "mur": {PerTransaction: 10_000.00, Daily: 45_000.00}, - "mvr": {PerTransaction: 2_500.00, Daily: 15_000.00}, - "mwk": {PerTransaction: 250_000.00, Daily: 1_000_000.00}, - "mxn": {PerTransaction: 2_500.00, Daily: 17_500.00}, - "myr": {PerTransaction: 500.00, Daily: 4_000.00}, - "mzn": {PerTransaction: 15_000.00, Daily: 60_000.00}, - "nad": {PerTransaction: 2_500.00, Daily: 15_000.00}, - "ngn": {PerTransaction: 100_000.00, Daily: 400_000.00}, - "nio": {PerTransaction: 5_000.00, Daily: 35_000.00}, - "nok": {PerTransaction: 2500.00, Daily: 9000.00}, - "npr": {PerTransaction: 25_000.00, Daily: 125_000.00}, - "nzd": {PerTransaction: 500.00, Daily: 1_500.00}, - "omr": {PerTransaction: 50.00, Daily: 350.00}, - "pab": {PerTransaction: 250.00, Daily: 1_000.00}, - "pen": {PerTransaction: 500.00, Daily: 3_500.00}, - "pgk": {PerTransaction: 1_000.00, Daily: 3_500.00}, - "php": {PerTransaction: 10_000.00, Daily: 50_000.00}, - "pkr": {PerTransaction: 50_000.00, Daily: 250_000.00}, - "pln": {PerTransaction: 750.00, Daily: 4_500.00}, - "pyg": {PerTransaction: 1_500_000.00, Daily: 6_500_000.00}, - "qar": {PerTransaction: 500.00, Daily: 3_500.00}, - "ron": {PerTransaction: 500.00, Daily: 4_500.00}, - "rsd": {PerTransaction: 25_000.00, Daily: 100_000.00}, - "rub": {PerTransaction: 15_000.00, Daily: 60_000.00}, - "rwf": {PerTransaction: 250_000.00, Daily: 1_000_000.00}, - "sar": {PerTransaction: 750.00, Daily: 3_750.00}, - "sbd": {PerTransaction: 2_000.00, Daily: 8_000.00}, - "scr": {PerTransaction: 3_000.00, Daily: 12_500.00}, - "sdg": {PerTransaction: 150_000.00, Daily: 500_000.00}, - "sek": {PerTransaction: 2_500.00, Daily: 10_000.00}, - "sgd": {PerTransaction: 250.00, Daily: 1_250.00}, - "shp": {PerTransaction: 250.00, Daily: 1_250.00}, - "sll": {PerTransaction: 5_000_000.00, Daily: 12_500_000.00}, - "sos": {PerTransaction: 150_000.00, Daily: 500_000.00}, - "srd": {PerTransaction: 5_000.00, Daily: 22_500.00}, - "std": {PerTransaction: 5_000_000.00, Daily: 20_000_000.00}, - "syp": {PerTransaction: 500_000.00, Daily: 2_500_000.00}, - "szl": {PerTransaction: 2_500.00, Daily: 15_000.00}, - "thb": {PerTransaction: 5_000.00, Daily: 35_000.00}, - "tjs": {PerTransaction: 2_500.00, Daily: 10_000.00}, - "tmt": {PerTransaction: 750.00, Daily: 3_500.00}, - "tnd": {PerTransaction: 750.00, Daily: 3_000.00}, - "top": {PerTransaction: 500.00, Daily: 2_250.00}, - "try": {PerTransaction: 2_500.00, Daily: 17_500.00}, - "ttd": {PerTransaction: 1_500.00, Daily: 6_500.00}, - "twd": {PerTransaction: 5_000.00, Daily: 30_000.00}, - "tzs": {PerTransaction: 500_000.00, Daily: 2_250_000.00}, - "uah": {PerTransaction: 10_000.00, Daily: 35_000.00}, - "ugx": {PerTransaction: 1_000_000.00, Daily: 3_500_000.00}, - "usd": {PerTransaction: 250.00, Daily: 1000.00}, - "uyu": {PerTransaction: 10_000.00, Daily: 40_000.00}, - "uzs": {PerTransaction: 2_500_000.00, Daily: 10_000_000.00}, - "vnd": {PerTransaction: 5_000_000.00, Daily: 22_500_000.00}, - "vuv": {PerTransaction: 25_000.00, Daily: 100_000.00}, - "wst": {PerTransaction: 500.00, Daily: 2_500.00}, - "xaf": {PerTransaction: 150_000.00, Daily: 600_000.00}, - "xcd": {PerTransaction: 500.00, Daily: 2_500.00}, - "xdr": {PerTransaction: 150.00, Daily: 750.00}, - "xof": {PerTransaction: 150_000.00, Daily: 600_000.00}, - "xpf": {PerTransaction: 25_000.00, Daily: 100_000.00}, - "yer": {PerTransaction: 50_000.00, Daily: 250_000.00}, - "zar": {PerTransaction: 2_500.00, Daily: 15_000.00}, - "zmk": {PerTransaction: 2_500_000.00, Daily: 9_000_000.00}, - "zmw": {PerTransaction: 2_500.00, Daily: 15_000.00}, - "zwl": {PerTransaction: 50_000.00, Daily: 300_000.00}, + "aed": {PerTransaction: 1_000.00, Daily: 3_500.00}, + "afn": {PerTransaction: 20_000.00, Daily: 85_000.00}, + "all": {PerTransaction: 25_000.00, Daily: 100_000.00}, + "amd": {PerTransaction: 100_000.00, Daily: 400_000.00}, + "ang": {PerTransaction: 500.00, Daily: 1_750.00}, + "aoa": {PerTransaction: 100_000.00, Daily: 500_000.00}, + "ars": {PerTransaction: 25_000.00, Daily: 125_000.00}, + "aud": {PerTransaction: 250.00, Daily: 1_250.00}, + "awg": {PerTransaction: 500.00, Daily: 1_500.00}, + "azn": {PerTransaction: 250.00, Daily: 1_500.00}, + "bam": {PerTransaction: 500.00, Daily: 1_500.00}, + "bbd": {PerTransaction: 500.00, Daily: 2_000.00}, + "bdt": {PerTransaction: 25_000.00, Daily: 90_000.00}, + "bgn": {PerTransaction: 500.00, Daily: 1_750.00}, + "bhd": {PerTransaction: 100.00, Daily: 350.00}, + "bif": {PerTransaction: 500_000.00, Daily: 2_000_000.00}, + "bmd": {PerTransaction: 250.00, Daily: 1_000.00}, + "bnd": {PerTransaction: 250.00, Daily: 1_250.00}, + "bob": {PerTransaction: 1_500.00, Daily: 6_500.00}, + "brl": {PerTransaction: 1_000.00, Daily: 5_000.00}, + "bsd": {PerTransaction: 250.00, Daily: 1_000.00}, + "btn": {PerTransaction: 20_000.00, Daily: 75_000.00}, + "bwp": {PerTransaction: 2_500.00, Daily: 10_000.00}, + "byn": {PerTransaction: 500.00, Daily: 2_500.00}, + "byr": {PerTransaction: 5_000_000.00, Daily: 17_500_000.00}, + "bzd": {PerTransaction: 500.00, Daily: 2_000.00}, + "cad": {PerTransaction: 250.00, Daily: 1_250.00}, + "cdf": {PerTransaction: 500_000.00, Daily: 2_000_000.00}, + "chf": {PerTransaction: 250.00, Daily: 900.00}, + "clf": {PerTransaction: 8.00, Daily: 30.00}, + "clp": {PerTransaction: 150_000.00, Daily: 800_000.00}, + "cny": {PerTransaction: 1_500.00, Daily: 6_500.00}, + "cop": {PerTransaction: 1_000_000.00, Daily: 4_000_000.00}, + "crc": {PerTransaction: 150_000.00, Daily: 60_0000.00}, + "cuc": {PerTransaction: 250.00, Daily: 1_000.00}, + "cup": {PerTransaction: 5_000.00, Daily: 25_000.00}, + "cve": {PerTransaction: 25_000.00, Daily: 100_000.00}, + "czk": {PerTransaction: 5_000.00, Daily: 22_500.00}, + "djf": {PerTransaction: 40_000.00, Daily: 175_000.00}, + "dkk": {PerTransaction: 1_000.00, Daily: 7_000.00}, + "dop": {PerTransaction: 12_500.00, Daily: 50_000.00}, + "dzd": {PerTransaction: 25_000.00, Daily: 125_000.00}, + "egp": {PerTransaction: 2_500.00, Daily: 17_500.00}, + "ern": {PerTransaction: 2_500.00, Daily: 15_000.00}, + "etb": {PerTransaction: 12_500.00, Daily: 50_000.00}, + "eur": {PerTransaction: 250.00, Daily: 999.99}, + "fjd": {PerTransaction: 500.00, Daily: 2_000.00}, + "fkp": {PerTransaction: 250.00, Daily: 800.00}, + "gbp": {PerTransaction: 250.00, Daily: 800.00}, + "gel": {PerTransaction: 500.00, Daily: 2_500.00}, + "ggp": {PerTransaction: 250.00, Daily: 800.00}, + "ghs": {PerTransaction: 2_250.00, Daily: 12_500.00}, + "gip": {PerTransaction: 250.00, Daily: 800.00}, + "gmd": {PerTransaction: 15_000.00, Daily: 50_000.00}, + "gnf": {PerTransaction: 2_000_000.00, Daily: 8_500_000.00}, + "gtq": {PerTransaction: 1_500.00, Daily: 7_500.00}, + "gyd": {PerTransaction: 50_000.00, Daily: 200_000.00}, + "hkd": {PerTransaction: 1_000.00, Daily: 7_500.00}, + "hnl": {PerTransaction: 5_000.00, Daily: 22_500.00}, + "hrk": {PerTransaction: 1_500.00, Daily: 7_250.00}, + "htg": {PerTransaction: 25_000.00, Daily: 125_000.00}, + "huf": {PerTransaction: 50_000.00, Daily: 375_000.00}, + "idr": {PerTransaction: 1_500_000.00, Daily: 14_000_000.00}, + "ils": {PerTransaction: 750.00, Daily: 3_000.00}, + "imp": {PerTransaction: 250.00, Daily: 800.00}, + "inr": {PerTransaction: 10_000.00, Daily: 80_000.00}, + "iqd": {PerTransaction: 250_000.00, Daily: 1_250_000.00}, + "irr": {PerTransaction: 10_000_000.00, Daily: 40_000_000.00}, + "isk": {PerTransaction: 25_000.00, Daily: 125_000.00}, + "jep": {PerTransaction: 250.00, Daily: 800.00}, + "jmd": {PerTransaction: 25_000.00, Daily: 150_000.00}, + "jod": {PerTransaction: 100.00, Daily: 700.00}, + "jpy": {PerTransaction: 25_000.00, Daily: 125_000.00}, + "kes": {PerTransaction: 25_000.00, Daily: 100_000.00}, + "kgs": {PerTransaction: 10_000.00, Daily: 80_000.00}, + "khr": {PerTransaction: 1_000_000.00, Daily: 4_000_000.00}, + "kmf": {PerTransaction: 100_000.00, Daily: 400_000.00}, + "kpw": {PerTransaction: 250_000.00, Daily: 900_000.00}, + "krw": {PerTransaction: 250_000.00, Daily: 1_250_000.00}, + "kwd": {PerTransaction: 50.00, Daily: 300.00}, + "kyd": {PerTransaction: 250.00, Daily: 800.00}, + "kzt": {PerTransaction: 100_000.00, Daily: 400_000.00}, + "lak": {PerTransaction: 2_500_000.00, Daily: 15_000_000.00}, + "lbp": {PerTransaction: 3_000_000.00, Daily: 15_000_000.00}, + "lkr": {PerTransaction: 50_000.00, Daily: 350_000.00}, + "lrd": {PerTransaction: 25_000.00, Daily: 150_000.00}, + "lsl": {PerTransaction: 2_500.00, Daily: 15_000.00}, + "ltl": {PerTransaction: 1_000.00, Daily: 2_500.00}, + "lvl": {PerTransaction: 150.00, Daily: 600.00}, + "lyd": {PerTransaction: 1_250.00, Daily: 4_500.00}, + "mad": {PerTransaction: 2_500.00, Daily: 10_000.00}, + "mdl": {PerTransaction: 2_500.00, Daily: 17_500.00}, + "mga": {PerTransaction: 1_000_000.00, Daily: 4_000_000.00}, + "mkd": {PerTransaction: 15_000.00, Daily: 50_000.00}, + "mmk": {PerTransaction: 500_000.00, Daily: 2_000_000.00}, + "mnt": {PerTransaction: 750_000.00, Daily: 3_000_000.00}, + "mop": {PerTransaction: 2_000.00, Daily: 8_000.00}, + "mru": {PerTransaction: 10000.00, Daily: 35000.00}, + "mur": {PerTransaction: 10_000.00, Daily: 45_000.00}, + "mvr": {PerTransaction: 2_500.00, Daily: 15_000.00}, + "mwk": {PerTransaction: 250_000.00, Daily: 1_000_000.00}, + "mxn": {PerTransaction: 2_500.00, Daily: 17_500.00}, + "myr": {PerTransaction: 500.00, Daily: 4_000.00}, + "mzn": {PerTransaction: 15_000.00, Daily: 60_000.00}, + "nad": {PerTransaction: 2_500.00, Daily: 15_000.00}, + "ngn": {PerTransaction: 100_000.00, Daily: 400_000.00}, + "nio": {PerTransaction: 5_000.00, Daily: 35_000.00}, + "nok": {PerTransaction: 2500.00, Daily: 9000.00}, + "npr": {PerTransaction: 25_000.00, Daily: 125_000.00}, + "nzd": {PerTransaction: 500.00, Daily: 1_500.00}, + "omr": {PerTransaction: 50.00, Daily: 350.00}, + "pab": {PerTransaction: 250.00, Daily: 1_000.00}, + "pen": {PerTransaction: 500.00, Daily: 3_500.00}, + "pgk": {PerTransaction: 1_000.00, Daily: 3_500.00}, + "php": {PerTransaction: 10_000.00, Daily: 50_000.00}, + "pkr": {PerTransaction: 50_000.00, Daily: 250_000.00}, + "pln": {PerTransaction: 750.00, Daily: 4_500.00}, + "pyg": {PerTransaction: 1_500_000.00, Daily: 6_500_000.00}, + "qar": {PerTransaction: 500.00, Daily: 3_500.00}, + "ron": {PerTransaction: 500.00, Daily: 4_500.00}, + "rsd": {PerTransaction: 25_000.00, Daily: 100_000.00}, + "rub": {PerTransaction: 15_000.00, Daily: 60_000.00}, + "rwf": {PerTransaction: 250_000.00, Daily: 1_000_000.00}, + "sar": {PerTransaction: 750.00, Daily: 3_750.00}, + "sbd": {PerTransaction: 2_000.00, Daily: 8_000.00}, + "scr": {PerTransaction: 3_000.00, Daily: 12_500.00}, + "sdg": {PerTransaction: 150_000.00, Daily: 500_000.00}, + "sek": {PerTransaction: 2_500.00, Daily: 10_000.00}, + "sgd": {PerTransaction: 250.00, Daily: 1_250.00}, + "shp": {PerTransaction: 250.00, Daily: 1_250.00}, + "sll": {PerTransaction: 5_000_000.00, Daily: 12_500_000.00}, + "sos": {PerTransaction: 150_000.00, Daily: 500_000.00}, + "srd": {PerTransaction: 5_000.00, Daily: 22_500.00}, + "std": {PerTransaction: 5_000_000.00, Daily: 20_000_000.00}, + "syp": {PerTransaction: 500_000.00, Daily: 2_500_000.00}, + "szl": {PerTransaction: 2_500.00, Daily: 15_000.00}, + "thb": {PerTransaction: 5_000.00, Daily: 35_000.00}, + "tjs": {PerTransaction: 2_500.00, Daily: 10_000.00}, + "tmt": {PerTransaction: 750.00, Daily: 3_500.00}, + "tnd": {PerTransaction: 750.00, Daily: 3_000.00}, + "top": {PerTransaction: 500.00, Daily: 2_250.00}, + "try": {PerTransaction: 2_500.00, Daily: 17_500.00}, + "ttd": {PerTransaction: 1_500.00, Daily: 6_500.00}, + "twd": {PerTransaction: 5_000.00, Daily: 30_000.00}, + "tzs": {PerTransaction: 500_000.00, Daily: 2_250_000.00}, + "uah": {PerTransaction: 10_000.00, Daily: 35_000.00}, + "ugx": {PerTransaction: 1_000_000.00, Daily: 3_500_000.00}, + "usd": {PerTransaction: 250.00, Daily: 1000.00}, + "usdc": {PerTransaction: 250.00, Daily: 1000.00}, + "uyu": {PerTransaction: 10_000.00, Daily: 40_000.00}, + "uzs": {PerTransaction: 2_500_000.00, Daily: 10_000_000.00}, + "vnd": {PerTransaction: 5_000_000.00, Daily: 22_500_000.00}, + "vuv": {PerTransaction: 25_000.00, Daily: 100_000.00}, + "wst": {PerTransaction: 500.00, Daily: 2_500.00}, + "xaf": {PerTransaction: 150_000.00, Daily: 600_000.00}, + "xcd": {PerTransaction: 500.00, Daily: 2_500.00}, + "xdr": {PerTransaction: 150.00, Daily: 750.00}, + "xof": {PerTransaction: 150_000.00, Daily: 600_000.00}, + "xpf": {PerTransaction: 25_000.00, Daily: 100_000.00}, + "yer": {PerTransaction: 50_000.00, Daily: 250_000.00}, + "zar": {PerTransaction: 2_500.00, Daily: 15_000.00}, + "zmk": {PerTransaction: 2_500_000.00, Daily: 9_000_000.00}, + "zmw": {PerTransaction: 2_500.00, Daily: 15_000.00}, + "zwl": {PerTransaction: 50_000.00, Daily: 300_000.00}, } MicroPaymentLimits = map[currency_lib.Code]MicroPaymentLimit{ - "aed": {Min: 0.20, Max: 4.00}, - "afn": {Min: 4.00, Max: 80.00}, - "all": {Min: 5.00, Max: 100.00}, - "amd": {Min: 20.0, Max: 400.00}, - "ang": {Min: 0.10, Max: 2.00}, - "aoa": {Min: 20.00, Max: 400.00}, - "ars": {Min: 5.00, Max: 100.00}, - "aud": {Min: 0.05, Max: 1.00}, - "awg": {Min: 0.10, Max: 2.00}, - "azn": {Min: 0.05, Max: 1.00}, - "bam": {Min: 0.10, Max: 2.00}, - "bbd": {Min: 0.10, Max: 2.00}, - "bdt": {Min: 5.00, Max: 100.00}, - "bgn": {Min: 0.10, Max: 2.00}, - "bhd": {Min: 0.02, Max: 0.40}, - "bif": {Min: 100.00, Max: 2000.00}, - "bmd": {Min: 0.05, Max: 1.00}, - "bnd": {Min: 0.05, Max: 1.00}, - "bob": {Min: 0.30, Max: 6.00}, - "brl": {Min: 0.20, Max: 4.00}, - "bsd": {Min: 0.05, Max: 1.00}, - "btn": {Min: 4.00, Max: 80.00}, - "bwp": {Min: 0.50, Max: 10.00}, - "byn": {Min: 0.10, Max: 2.00}, - "byr": {Min: 1000.0, Max: 20000.00}, - "bzd": {Min: 0.10, Max: 2.00}, - "cad": {Min: 0.05, Max: 1.00}, - "cdf": {Min: 100.00, Max: 2000.00}, - "chf": {Min: 0.05, Max: 1.00}, - "clf": {Min: 0.01, Max: 0.03}, - "clp": {Min: 30.00, Max: 600.00}, - "cny": {Min: 0.30, Max: 6.00}, - "cop": {Min: 200.00, Max: 4000.00}, - "crc": {Min: 30.00, Max: 600.00}, - "cuc": {Min: 0.05, Max: 1.00}, - "cup": {Min: 1.00, Max: 20.00}, - "cve": {Min: 5.00, Max: 100.00}, - "czk": {Min: 1.00, Max: 20.00}, - "djf": {Min: 8.00, Max: 160.00}, - "dkk": {Min: 0.20, Max: 4.00}, - "dop": {Min: 2.50, Max: 50.00}, - "dzd": {Min: 5.00, Max: 100.00}, - "egp": {Min: 0.50, Max: 10.00}, - "ern": {Min: 0.50, Max: 10.00}, - "etb": {Min: 2.50, Max: 50.00}, - "eur": {Min: 0.05, Max: 1.00}, - "fjd": {Min: 0.10, Max: 2.00}, - "fkp": {Min: 0.05, Max: 1.00}, - "gbp": {Min: 0.05, Max: 1.00}, - "gel": {Min: 0.10, Max: 2.00}, - "ggp": {Min: 0.05, Max: 1.00}, - "ghs": {Min: 0.45, Max: 9.00}, - "gip": {Min: 0.05, Max: 1.00}, - "gmd": {Min: 3.00, Max: 60.00}, - "gnf": {Min: 400.00, Max: 8000.00}, - "gtq": {Min: 0.30, Max: 6.00}, - "gyd": {Min: 10.00, Max: 200.00}, - "hkd": {Min: 0.20, Max: 4.00}, - "hnl": {Min: 1.00, Max: 20.00}, - "hrk": {Min: 0.30, Max: 6.00}, - "htg": {Min: 5.00, Max: 100.00}, - "huf": {Min: 10.00, Max: 200.00}, - "idr": {Min: 300.00, Max: 6000.00}, - "ils": {Min: 0.15, Max: 3.00}, - "imp": {Min: 0.05, Max: 1.00}, - "inr": {Min: 2.00, Max: 40.00}, - "iqd": {Min: 50.00, Max: 1000.00}, - "irr": {Min: 2000.00, Max: 40000.00}, - "isk": {Min: 5.00, Max: 100.00}, - "jep": {Min: 0.05, Max: 1.00}, - "jmd": {Min: 5.00, Max: 100.00}, - "jod": {Min: 0.02, Max: 0.40}, - "jpy": {Min: 5.00, Max: 100.00}, - "kes": {Min: 5.00, Max: 100.00}, - "kgs": {Min: 2.00, Max: 40.00}, - "khr": {Min: 200.00, Max: 4000.00}, - "kin": {Min: 2500.00, Max: 50000.00}, - "kmf": {Min: 20.00, Max: 400.00}, - "kpw": {Min: 50.00, Max: 1000.00}, - "krw": {Min: 50.00, Max: 1000.00}, - "kwd": {Min: 0.01, Max: 0.20}, - "kyd": {Min: 0.05, Max: 1.00}, - "kzt": {Min: 20.00, Max: 400.00}, - "lak": {Min: 500.00, Max: 10000.00}, - "lbp": {Min: 600.00, Max: 12000.00}, - "lkr": {Min: 10.00, Max: 200.00}, - "lrd": {Min: 5.00, Max: 100.00}, - "lsl": {Min: 0.50, Max: 10.00}, - "ltl": {Min: 0.20, Max: 4.00}, - "lvl": {Min: 0.03, Max: 0.60}, - "lyd": {Min: 0.25, Max: 5.00}, - "mad": {Min: 0.50, Max: 10.00}, - "mdl": {Min: 0.50, Max: 10.00}, - "mga": {Min: 200.00, Max: 4000.00}, - "mkd": {Min: 3.00, Max: 60.00}, - "mmk": {Min: 100.00, Max: 2000.00}, - "mnt": {Min: 150.00, Max: 3000.00}, - "mop": {Min: 0.40, Max: 8.00}, - "mru": {Min: 2.00, Max: 40.00}, - "mur": {Min: 2.00, Max: 40.00}, - "mvr": {Min: 0.50, Max: 10.00}, - "mwk": {Min: 50.00, Max: 1000.00}, - "mxn": {Min: 0.50, Max: 10.00}, - "myr": {Min: 0.10, Max: 2.00}, - "mzn": {Min: 3.00, Max: 60.00}, - "nad": {Min: 0.50, Max: 10.00}, - "ngn": {Min: 20.00, Max: 400.00}, - "nio": {Min: 1.00, Max: 20.00}, - "nok": {Min: 0.50, Max: 10.00}, - "npr": {Min: 5.00, Max: 100.00}, - "nzd": {Min: 0.10, Max: 2.00}, - "omr": {Min: 0.01, Max: 0.20}, - "pab": {Min: 0.05, Max: 1.00}, - "pen": {Min: 0.10, Max: 2.00}, - "pgk": {Min: 0.20, Max: 4.00}, - "php": {Min: 2.00, Max: 40.00}, - "pkr": {Min: 10.00, Max: 200.00}, - "pln": {Min: 0.15, Max: 3.00}, - "pyg": {Min: 300.00, Max: 6000.00}, - "qar": {Min: 0.10, Max: 2.00}, - "ron": {Min: 0.10, Max: 2.00}, - "rsd": {Min: 5.00, Max: 100.00}, - "rub": {Min: 3.00, Max: 60.00}, - "rwf": {Min: 50.00, Max: 1000.00}, - "sar": {Min: 0.15, Max: 3.00}, - "sbd": {Min: 0.40, Max: 8.00}, - "scr": {Min: 0.60, Max: 12.00}, - "sdg": {Min: 30.00, Max: 600.00}, - "sek": {Min: 0.50, Max: 10.00}, - "sgd": {Min: 0.05, Max: 1.00}, - "shp": {Min: 0.05, Max: 1.00}, - "sll": {Min: 1000.00, Max: 20000.00}, - "sos": {Min: 30.00, Max: 600.00}, - "srd": {Min: 1.00, Max: 20.00}, - "std": {Min: 1000.00, Max: 20000.00}, - "syp": {Min: 100.00, Max: 2000.00}, - "szl": {Min: 0.50, Max: 10.00}, - "thb": {Min: 1.00, Max: 20.00}, - "tjs": {Min: 0.50, Max: 10.00}, - "tmt": {Min: 0.15, Max: 3.00}, - "tnd": {Min: 0.15, Max: 3.00}, - "top": {Min: 0.10, Max: 2.00}, - "try": {Min: 0.50, Max: 10.00}, - "ttd": {Min: 0.30, Max: 6.00}, - "twd": {Min: 1.00, Max: 20.00}, - "tzs": {Min: 100.00, Max: 2000.00}, - "uah": {Min: 2.00, Max: 40.00}, - "ugx": {Min: 200.00, Max: 4000.00}, - "usd": {Min: 0.05, Max: 1.00}, - "uyu": {Min: 2.00, Max: 40.00}, - "uzs": {Min: 500.00, Max: 10000.00}, - "vnd": {Min: 1000.00, Max: 20000.00}, - "vuv": {Min: 5.00, Max: 100.00}, - "wst": {Min: 0.10, Max: 2.00}, - "xaf": {Min: 30.00, Max: 600.00}, - "xcd": {Min: 0.10, Max: 2.00}, - "xdr": {Min: 0.03, Max: 0.60}, - "xof": {Min: 30.00, Max: 600.00}, - "xpf": {Min: 5.00, Max: 100.00}, - "yer": {Min: 10.00, Max: 200.00}, - "zar": {Min: 0.50, Max: 10.00}, - "zmk": {Min: 500.00, Max: 10000.00}, - "zmw": {Min: 0.50, Max: 10.00}, - "zwl": {Min: 10.00, Max: 200.00}, + "aed": {Min: 0.20, Max: 4.00}, + "afn": {Min: 4.00, Max: 80.00}, + "all": {Min: 5.00, Max: 100.00}, + "amd": {Min: 20.0, Max: 400.00}, + "ang": {Min: 0.10, Max: 2.00}, + "aoa": {Min: 20.00, Max: 400.00}, + "ars": {Min: 5.00, Max: 100.00}, + "aud": {Min: 0.05, Max: 1.00}, + "awg": {Min: 0.10, Max: 2.00}, + "azn": {Min: 0.05, Max: 1.00}, + "bam": {Min: 0.10, Max: 2.00}, + "bbd": {Min: 0.10, Max: 2.00}, + "bdt": {Min: 5.00, Max: 100.00}, + "bgn": {Min: 0.10, Max: 2.00}, + "bhd": {Min: 0.02, Max: 0.40}, + "bif": {Min: 100.00, Max: 2000.00}, + "bmd": {Min: 0.05, Max: 1.00}, + "bnd": {Min: 0.05, Max: 1.00}, + "bob": {Min: 0.30, Max: 6.00}, + "brl": {Min: 0.20, Max: 4.00}, + "bsd": {Min: 0.05, Max: 1.00}, + "btn": {Min: 4.00, Max: 80.00}, + "bwp": {Min: 0.50, Max: 10.00}, + "byn": {Min: 0.10, Max: 2.00}, + "byr": {Min: 1000.0, Max: 20000.00}, + "bzd": {Min: 0.10, Max: 2.00}, + "cad": {Min: 0.05, Max: 1.00}, + "cdf": {Min: 100.00, Max: 2000.00}, + "chf": {Min: 0.05, Max: 1.00}, + "clf": {Min: 0.01, Max: 0.03}, + "clp": {Min: 30.00, Max: 600.00}, + "cny": {Min: 0.30, Max: 6.00}, + "cop": {Min: 200.00, Max: 4000.00}, + "crc": {Min: 30.00, Max: 600.00}, + "cuc": {Min: 0.05, Max: 1.00}, + "cup": {Min: 1.00, Max: 20.00}, + "cve": {Min: 5.00, Max: 100.00}, + "czk": {Min: 1.00, Max: 20.00}, + "djf": {Min: 8.00, Max: 160.00}, + "dkk": {Min: 0.20, Max: 4.00}, + "dop": {Min: 2.50, Max: 50.00}, + "dzd": {Min: 5.00, Max: 100.00}, + "egp": {Min: 0.50, Max: 10.00}, + "ern": {Min: 0.50, Max: 10.00}, + "etb": {Min: 2.50, Max: 50.00}, + "eur": {Min: 0.05, Max: 1.00}, + "fjd": {Min: 0.10, Max: 2.00}, + "fkp": {Min: 0.05, Max: 1.00}, + "gbp": {Min: 0.05, Max: 1.00}, + "gel": {Min: 0.10, Max: 2.00}, + "ggp": {Min: 0.05, Max: 1.00}, + "ghs": {Min: 0.45, Max: 9.00}, + "gip": {Min: 0.05, Max: 1.00}, + "gmd": {Min: 3.00, Max: 60.00}, + "gnf": {Min: 400.00, Max: 8000.00}, + "gtq": {Min: 0.30, Max: 6.00}, + "gyd": {Min: 10.00, Max: 200.00}, + "hkd": {Min: 0.20, Max: 4.00}, + "hnl": {Min: 1.00, Max: 20.00}, + "hrk": {Min: 0.30, Max: 6.00}, + "htg": {Min: 5.00, Max: 100.00}, + "huf": {Min: 10.00, Max: 200.00}, + "idr": {Min: 300.00, Max: 6000.00}, + "ils": {Min: 0.15, Max: 3.00}, + "imp": {Min: 0.05, Max: 1.00}, + "inr": {Min: 2.00, Max: 40.00}, + "iqd": {Min: 50.00, Max: 1000.00}, + "irr": {Min: 2000.00, Max: 40000.00}, + "isk": {Min: 5.00, Max: 100.00}, + "jep": {Min: 0.05, Max: 1.00}, + "jmd": {Min: 5.00, Max: 100.00}, + "jod": {Min: 0.02, Max: 0.40}, + "jpy": {Min: 5.00, Max: 100.00}, + "kes": {Min: 5.00, Max: 100.00}, + "kgs": {Min: 2.00, Max: 40.00}, + "khr": {Min: 200.00, Max: 4000.00}, + "kmf": {Min: 20.00, Max: 400.00}, + "kpw": {Min: 50.00, Max: 1000.00}, + "krw": {Min: 50.00, Max: 1000.00}, + "kwd": {Min: 0.01, Max: 0.20}, + "kyd": {Min: 0.05, Max: 1.00}, + "kzt": {Min: 20.00, Max: 400.00}, + "lak": {Min: 500.00, Max: 10000.00}, + "lbp": {Min: 600.00, Max: 12000.00}, + "lkr": {Min: 10.00, Max: 200.00}, + "lrd": {Min: 5.00, Max: 100.00}, + "lsl": {Min: 0.50, Max: 10.00}, + "ltl": {Min: 0.20, Max: 4.00}, + "lvl": {Min: 0.03, Max: 0.60}, + "lyd": {Min: 0.25, Max: 5.00}, + "mad": {Min: 0.50, Max: 10.00}, + "mdl": {Min: 0.50, Max: 10.00}, + "mga": {Min: 200.00, Max: 4000.00}, + "mkd": {Min: 3.00, Max: 60.00}, + "mmk": {Min: 100.00, Max: 2000.00}, + "mnt": {Min: 150.00, Max: 3000.00}, + "mop": {Min: 0.40, Max: 8.00}, + "mru": {Min: 2.00, Max: 40.00}, + "mur": {Min: 2.00, Max: 40.00}, + "mvr": {Min: 0.50, Max: 10.00}, + "mwk": {Min: 50.00, Max: 1000.00}, + "mxn": {Min: 0.50, Max: 10.00}, + "myr": {Min: 0.10, Max: 2.00}, + "mzn": {Min: 3.00, Max: 60.00}, + "nad": {Min: 0.50, Max: 10.00}, + "ngn": {Min: 20.00, Max: 400.00}, + "nio": {Min: 1.00, Max: 20.00}, + "nok": {Min: 0.50, Max: 10.00}, + "npr": {Min: 5.00, Max: 100.00}, + "nzd": {Min: 0.10, Max: 2.00}, + "omr": {Min: 0.01, Max: 0.20}, + "pab": {Min: 0.05, Max: 1.00}, + "pen": {Min: 0.10, Max: 2.00}, + "pgk": {Min: 0.20, Max: 4.00}, + "php": {Min: 2.00, Max: 40.00}, + "pkr": {Min: 10.00, Max: 200.00}, + "pln": {Min: 0.15, Max: 3.00}, + "pyg": {Min: 300.00, Max: 6000.00}, + "qar": {Min: 0.10, Max: 2.00}, + "ron": {Min: 0.10, Max: 2.00}, + "rsd": {Min: 5.00, Max: 100.00}, + "rub": {Min: 3.00, Max: 60.00}, + "rwf": {Min: 50.00, Max: 1000.00}, + "sar": {Min: 0.15, Max: 3.00}, + "sbd": {Min: 0.40, Max: 8.00}, + "scr": {Min: 0.60, Max: 12.00}, + "sdg": {Min: 30.00, Max: 600.00}, + "sek": {Min: 0.50, Max: 10.00}, + "sgd": {Min: 0.05, Max: 1.00}, + "shp": {Min: 0.05, Max: 1.00}, + "sll": {Min: 1000.00, Max: 20000.00}, + "sos": {Min: 30.00, Max: 600.00}, + "srd": {Min: 1.00, Max: 20.00}, + "std": {Min: 1000.00, Max: 20000.00}, + "syp": {Min: 100.00, Max: 2000.00}, + "szl": {Min: 0.50, Max: 10.00}, + "thb": {Min: 1.00, Max: 20.00}, + "tjs": {Min: 0.50, Max: 10.00}, + "tmt": {Min: 0.15, Max: 3.00}, + "tnd": {Min: 0.15, Max: 3.00}, + "top": {Min: 0.10, Max: 2.00}, + "try": {Min: 0.50, Max: 10.00}, + "ttd": {Min: 0.30, Max: 6.00}, + "twd": {Min: 1.00, Max: 20.00}, + "tzs": {Min: 100.00, Max: 2000.00}, + "uah": {Min: 2.00, Max: 40.00}, + "ugx": {Min: 200.00, Max: 4000.00}, + "usd": {Min: 0.05, Max: 1.00}, + "usdc": {Min: 0.05, Max: 1.00}, + "uyu": {Min: 2.00, Max: 40.00}, + "uzs": {Min: 500.00, Max: 10000.00}, + "vnd": {Min: 1000.00, Max: 20000.00}, + "vuv": {Min: 5.00, Max: 100.00}, + "wst": {Min: 0.10, Max: 2.00}, + "xaf": {Min: 30.00, Max: 600.00}, + "xcd": {Min: 0.10, Max: 2.00}, + "xdr": {Min: 0.03, Max: 0.60}, + "xof": {Min: 30.00, Max: 600.00}, + "xpf": {Min: 5.00, Max: 100.00}, + "yer": {Min: 10.00, Max: 200.00}, + "zar": {Min: 0.50, Max: 10.00}, + "zmk": {Min: 500.00, Max: 10000.00}, + "zmw": {Min: 0.50, Max: 10.00}, + "zwl": {Min: 10.00, Max: 200.00}, } MaxDailyDepositUsdAmount = 1.5 * SendLimits[currency_lib.USD].Daily diff --git a/pkg/code/localization/currency.go b/pkg/code/localization/currency.go deleted file mode 100644 index ecc4ff29..00000000 --- a/pkg/code/localization/currency.go +++ /dev/null @@ -1,236 +0,0 @@ -package localization - -import ( - "strings" - - "golang.org/x/text/language" - "golang.org/x/text/message" - "golang.org/x/text/number" - - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/pkg/errors" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" -) - -var symbolByCode = map[currency_lib.Code]string{ - currency_lib.AED: "د.إ", - currency_lib.AFN: "؋", - currency_lib.ALL: "Lek", - currency_lib.ANG: "ƒ", - currency_lib.AOA: "Kz", - currency_lib.ARS: "$", - currency_lib.AUD: "$", - currency_lib.AWG: "ƒ", - currency_lib.AZN: "₼", - currency_lib.BAM: "KM", - currency_lib.BDT: "৳", - currency_lib.BBD: "$", - currency_lib.BGN: "лв", - currency_lib.BMD: "$", - currency_lib.BND: "$", - currency_lib.BOB: "$b", - currency_lib.BRL: "R$", - currency_lib.BSD: "$", - currency_lib.BWP: "P", - currency_lib.BYN: "Br", - currency_lib.BZD: "BZ$", - currency_lib.CAD: "$", - currency_lib.CHF: "CHF", - currency_lib.CLP: "$", - currency_lib.CNY: "¥", - currency_lib.COP: "$", - currency_lib.CRC: "₡", - currency_lib.CUC: "$", - currency_lib.CUP: "₱", - currency_lib.CZK: "Kč", - currency_lib.DKK: "kr", - currency_lib.DOP: "RD$", - currency_lib.EGP: "£", - currency_lib.ERN: "£", - currency_lib.EUR: "€", - currency_lib.FJD: "$", - currency_lib.FKP: "£", - currency_lib.GBP: "£", - currency_lib.GEL: "₾", - currency_lib.GGP: "£", - currency_lib.GHS: "¢", - currency_lib.GIP: "£", - currency_lib.GNF: "FG", - currency_lib.GTQ: "Q", - currency_lib.GYD: "$", - currency_lib.HKD: "$", - currency_lib.HNL: "L", - currency_lib.HRK: "kn", - currency_lib.HUF: "Ft", - currency_lib.IDR: "Rp", - currency_lib.ILS: "₪", - currency_lib.IMP: "£", - currency_lib.INR: "₹", - currency_lib.IRR: "﷼", - currency_lib.ISK: "kr", - currency_lib.JEP: "£", - currency_lib.JMD: "J$", - currency_lib.JPY: "¥", - currency_lib.KGS: "лв", - currency_lib.KHR: "៛", - currency_lib.KMF: "CF", - currency_lib.KPW: "₩", - currency_lib.KRW: "₩", - currency_lib.KYD: "$", - currency_lib.KZT: "лв", - currency_lib.LAK: "₭", - currency_lib.LBP: "£", - currency_lib.LKR: "₨", - currency_lib.LRD: "$", - currency_lib.LTL: "Lt", - currency_lib.LVL: "Ls", - currency_lib.MGA: "Ar", - currency_lib.MKD: "ден", - currency_lib.MMK: "K", - currency_lib.MNT: "₮", - currency_lib.MUR: "₨", - currency_lib.MXN: "$", - currency_lib.MYR: "RM", - currency_lib.MZN: "MT", - currency_lib.NAD: "$", - currency_lib.NGN: "₦", - currency_lib.NIO: "C$", - currency_lib.NOK: "kr", - currency_lib.NPR: "₨", - currency_lib.NZD: "$", - currency_lib.OMR: "﷼", - currency_lib.PAB: "B/.", - currency_lib.PEN: "S/.", - currency_lib.PHP: "₱", - currency_lib.PKR: "₨", - currency_lib.PLN: "zł", - currency_lib.PYG: "Gs", - currency_lib.QAR: "﷼", - currency_lib.RON: "lei", - currency_lib.RSD: "Дин.", - currency_lib.RUB: "₽", - currency_lib.RWF: "RF", - currency_lib.SAR: "﷼", - currency_lib.SBD: "$", - currency_lib.SCR: "₨", - currency_lib.SEK: "kr", - currency_lib.SGD: "$", - currency_lib.SHP: "£", - currency_lib.SOS: "S", - currency_lib.SRD: "$", - currency_lib.SSP: "£", - currency_lib.STD: "Db", - currency_lib.SVC: "$", - currency_lib.SYP: "£", - currency_lib.THB: "฿", - currency_lib.TOP: "T$", - currency_lib.TRY: "₺", - currency_lib.TTD: "TT$", - currency_lib.TWD: "NT$", - currency_lib.UAH: "₴", - currency_lib.USD: "$", - currency_lib.UYU: "$U", - currency_lib.UZS: "лв", - currency_lib.VND: "₫", - currency_lib.XCD: "$", - currency_lib.YER: "﷼", - currency_lib.ZAR: "R", - currency_lib.ZMW: "ZK", -} - -// FormatFiat formats a currency amount into a string in the provided locale -func FormatFiat(locale language.Tag, code currency_lib.Code, amount float64, ofKin bool) (string, error) { - isRtlScript := isRtlScript(locale) - - decimals := 2 - if code == currency_lib.KIN { - decimals = 0 - amount = float64(uint64(amount)) - } - - printer := message.NewPrinter(locale) - amountAsDecimal := number.Decimal(amount, number.Scale(decimals)) - formattedAmount := printer.Sprint(amountAsDecimal) - - symbol := symbolByCode[code] - - suffixKey := CoreOfKin - if code == currency_lib.KIN { - suffixKey = CoreKin - } - - localizedSuffix, localizedIn, err := localizeKey(locale, suffixKey) - if err != nil { - return "", err - } - if !ofKin { - localizedSuffix = "" - } - localizedSuffix = strings.TrimSpace(localizedSuffix) - - if isRtlScript && !isDefaultLocale(*localizedIn) { - if len(localizedSuffix) > 0 { - localizedSuffix = localizedSuffix + " " - } - return localizedSuffix + formattedAmount + symbol, nil - } - - if len(localizedSuffix) > 0 { - localizedSuffix = " " + localizedSuffix - } - return symbol + formattedAmount + localizedSuffix, nil -} - -// LocalizeFiatWithVerb is like FormatFiat, but includes a verb for the interaction -// with the currency amount -func LocalizeFiatWithVerb(locale language.Tag, verb chatpb.ExchangeDataContent_Verb, code currency_lib.Code, amount float64, ofKin bool) (string, error) { - localizedAmount, err := FormatFiat(locale, code, amount, ofKin) - if err != nil { - return "", err - } - - var key string - var isAmountBeforeVerb bool - switch verb { - case chatpb.ExchangeDataContent_GAVE: - key = VerbGave - case chatpb.ExchangeDataContent_RECEIVED: - key = VerbReceived - case chatpb.ExchangeDataContent_WITHDREW: - key = VerbWithdrew - case chatpb.ExchangeDataContent_DEPOSITED: - key = VerbDeposited - case chatpb.ExchangeDataContent_SENT: - key = VerbSpent - case chatpb.ExchangeDataContent_RETURNED: - key = VerbReturned - isAmountBeforeVerb = true - case chatpb.ExchangeDataContent_SPENT: - key = VerbSpent - case chatpb.ExchangeDataContent_PAID: - key = VerbPaid - case chatpb.ExchangeDataContent_PURCHASED: - key = VerbPurchased - case chatpb.ExchangeDataContent_RECEIVED_TIP: - key = VerbReceivedTip - case chatpb.ExchangeDataContent_SENT_TIP: - key = VerbSentTip - default: - return "", errors.Errorf("verb %s is not supported", verb) - } - - localizedVerbText, localizedIn, err := localizeKey(locale, key) - if err != nil { - return "", err - } - - if isRtlScript(locale) && !isDefaultLocale(*localizedIn) { - isAmountBeforeVerb = !isAmountBeforeVerb - } - - if isAmountBeforeVerb { - return localizedAmount + " " + localizedVerbText, nil - } - return localizedVerbText + " " + localizedAmount, nil -} diff --git a/pkg/code/localization/device.go b/pkg/code/localization/device.go deleted file mode 100644 index aefe7e76..00000000 --- a/pkg/code/localization/device.go +++ /dev/null @@ -1,37 +0,0 @@ -package localization - -import ( - "context" - "strings" - - "github.com/code-payments/code-server/pkg/grpc/client" -) - -// GetLocalizationKeyForUserAgent gets a localization key in the format for the device -// as provided in the user agent header. If an unknown device type, or the user- gent -// header isn't available, then the original key is returned. -func GetLocalizationKeyForUserAgent(ctx context.Context, key string) string { - userAgent, err := client.GetUserAgent(ctx) - if err != nil { - return key - } - - switch userAgent.DeviceType { - case client.DeviceTypeIOS: - return GetIosLocalizationKey(key) - case client.DeviceTypeAndroid: - return GetAndroidLocalizationKey(key) - } - - return key -} - -// GetIosLocalizationKey gets a localization string in the iOS format -func GetIosLocalizationKey(key string) string { - return strings.Replace(key, "_", ".", -1) -} - -// GetAndroidLocalizationKey gets a localization string in the Android format -func GetAndroidLocalizationKey(key string) string { - return strings.Replace(key, ".", "_", -1) -} diff --git a/pkg/code/localization/keys.go b/pkg/code/localization/keys.go deleted file mode 100644 index edd557df..00000000 --- a/pkg/code/localization/keys.go +++ /dev/null @@ -1,191 +0,0 @@ -package localization - -import ( - "errors" - "os" - "strings" - "sync" - - "github.com/nicksnyder/go-i18n/v2/i18n" - "golang.org/x/text/language" -) - -const ( - - // - // Section: Core - // - - CoreOfKin = "core.ofKin" - CoreKin = "core.kin" - - // - // Section: Pushes - // - - PushTitleDepositReceived = "push.title.depositReceived" - PushSubtitleDepositReceived = "push.subtitle.depositReceived" - - PushTitleKinReturned = "push.title.kinReturned" - PushSubtitleKinReturned = "push.subtitle.kinReturned" - - PushTitleTwitterAccountConnected = "push.title.twitterAccountConnected" - PushSubtitleTwitterAccountConnected = "push.subtitle.twitterAccountConnected" - - // - // Section: Chats - // - - // Chat Titles - - ChatTitleCashTransactions = "title.chat.cashTransactions" - ChatTitleCodeTeam = "title.chat.codeTeam" - ChatTitleKinPurchases = "title.chat.kinPurchases" - ChatTitlePayments = "title.chat.payments" - ChatTitleTips = "title.chat.tips" - - // Message Bodies - - ChatMessageReferralBonus = "subtitle.chat.referralBonus" - ChatMessageWelcomeBonus = "subtitle.chat.welcomeBonus" - - ChatMessageUsdcDeposited = "subtitle.chat.usdcDeposited" - ChatMessageUsdcBeingConverted = "subtitle.chat.usdcBeingConverted" - ChatMessageKinAvailableForUse = "subtitle.chat.kinAvailableForUse" - - // - // Verbs - // - - VerbGave = "subtitle.youGave" - VerbReceived = "subtitle.youReceived" - VerbWithdrew = "subtitle.youWithdrew" - VerbDeposited = "subtitle.youDeposited" - VerbSent = "subtitle.youSent" - VerbSpent = "subtitle.youSpent" - VerbPaid = "subtitle.youPaid" - VerbPurchased = "subtitle.youPurchased" - VerbReturned = "subtitle.wasReturnedToYou" - VerbReceivedTip = "subtitle.someoneTippedYou" - VerbSentTip = "subtitle.youTipped" -) - -var ( - bundleMu sync.RWMutex - bundle *i18n.Bundle - - defaultLocale = language.English -) - -// LoadKeys loads localization key-value pairs from the provided directory. -// -// todo: we'll want to improve this, but just getting something quick up to setup his localization package. -func LoadKeys(directory string) error { - if !strings.HasSuffix(directory, "/") { - directory = directory + "/" - } - - bundleMu.Lock() - defer bundleMu.Unlock() - - newBundle := i18n.NewBundle(defaultLocale) - - dirEntries, err := os.ReadDir(directory) - if err != nil { - return err - } - - for _, dirEntry := range dirEntries { - if !dirEntry.IsDir() && strings.HasSuffix(dirEntry.Name(), ".json") { - _, err = newBundle.LoadMessageFile(directory + dirEntry.Name()) - if err != nil { - return err - } - } - } - - bundle = newBundle - return nil -} - -// LoadTestKeys is a utility for injecting test localization keys -func LoadTestKeys(kvsByLocale map[language.Tag]map[string]string) { - bundleMu.Lock() - defer bundleMu.Unlock() - - newBundle := i18n.NewBundle(language.English) - - for locale, kvs := range kvsByLocale { - messages := make([]*i18n.Message, 0) - for k, v := range kvs { - messages = append(messages, &i18n.Message{ - ID: k, - Other: v, - }) - } - newBundle.AddMessages(locale, messages...) - } - - bundle = newBundle -} - -// ResetKeys resets localization to an empty mapping -func ResetKeys() { - bundleMu.Lock() - defer bundleMu.Unlock() - - bundle = i18n.NewBundle(language.English) -} - -func localizeKey(locale language.Tag, key string, args ...string) (string, *language.Tag, error) { - bundleMu.RLock() - defer bundleMu.RUnlock() - - if bundle == nil { - return "", nil, errors.New("localization bundle not configured") - } - - localizeConfigInvite := i18n.LocalizeConfig{ - MessageID: key, - } - - localizer := i18n.NewLocalizer(bundle, locale.String()) - localized, tag, err := localizer.LocalizeWithTag(&localizeConfigInvite) - switch err.(type) { - case *i18n.MessageNotFoundErr: - // Fall back to default locale if the key doesn't exist for the requested - // locale - localizer := i18n.NewLocalizer(bundle, defaultLocale.String()) - localized, tag, err = localizer.LocalizeWithTag(&localizeConfigInvite) - if err != nil { - return "", nil, err - } - case nil: - default: - return "", nil, err - } - - for _, arg := range args { - localized = strings.Replace(localized, "%@", arg, 1) - } - return localized, &tag, nil -} - -// Localize localizes a key to the corresponding string in the provided locale. -// An optional set of string parameters can be provided to be replaced in the string. -// Currenctly, these arguments must be localized outside of this function. -// -// todo: Generic argument handling, so all localization can happen in here -func Localize(locale language.Tag, key string, args ...string) (string, error) { - localized, _, err := localizeKey(locale, key, args...) - return localized, err -} - -// LocalizeWithFallback is like Localize, but returns defaultValue on error. -func LocalizeWithFallback(locale language.Tag, defaultValue, key string, args ...string) string { - localized, _, err := localizeKey(locale, key, args...) - if err != nil { - return defaultValue - } - return localized -} diff --git a/pkg/code/localization/util.go b/pkg/code/localization/util.go deleted file mode 100644 index b7fe983c..00000000 --- a/pkg/code/localization/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package localization - -import "golang.org/x/text/language" - -func isRtlScript(t language.Tag) bool { - script, _ := t.Script() - switch script.String() { - case - "Adlm", - "Arab", - "Aran", - "Hebr", - "Mand", - "Mend", - "Nkoo", - "Rohg", - "Samr", - "Syrc", - "Syre", - "Syrj", - "Syrn", - "Thaa": - return true - } - return false -} - -func isDefaultLocale(locale language.Tag) bool { - return locale.String() == defaultLocale.String() -} diff --git a/pkg/code/push/badge_count.go b/pkg/code/push/badge_count.go deleted file mode 100644 index b2465be4..00000000 --- a/pkg/code/push/badge_count.go +++ /dev/null @@ -1,101 +0,0 @@ -package push - -import ( - "context" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/badgecount" - "github.com/code-payments/code-server/pkg/code/data/login" - push_data "github.com/code-payments/code-server/pkg/code/data/push" - push_lib "github.com/code-payments/code-server/pkg/push" -) - -// UpdateBadgeCount updates the badge count for an owner account to the latest value -// -// todo: Duplicated code with other send push utitilies -func UpdateBadgeCount( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - owner *common.Account, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "SetBadgeCount", - "owner": owner.PublicKey().ToBase58(), - }) - - // todo: Propagate this logic to other push sending utilities once login - // detection is made public. - loginRecord, err := data.GetLatestLoginByOwner(ctx, owner.PublicKey().ToBase58()) - if err == login.ErrLoginNotFound { - return nil - } else if err != nil { - log.WithError(err).Warn("failure getting login record") - return err - } - - var badgeCount int - badgeCountRecord, err := data.GetBadgeCount(ctx, owner.PublicKey().ToBase58()) - if err == nil { - badgeCount = int(badgeCountRecord.BadgeCount) - } else if err != badgecount.ErrBadgeCountNotFound { - log.WithError(err).Warn("failure getting badge count record") - return err - } - - pushTokenRecords, err := getPushTokensForOwner(ctx, data, owner) - if err != nil { - log.WithError(err).Warn("failure getting push tokens for owner") - return err - } - - seenPushTokens := make(map[string]struct{}) - for _, pushTokenRecord := range pushTokenRecords { - // Dedup push tokens, since they may appear more than once per app install - if _, ok := seenPushTokens[pushTokenRecord.PushToken]; ok { - continue - } - - switch pushTokenRecord.TokenType { - case push_data.TokenTypeFcmApns: - log := log.WithField("push_token", pushTokenRecord.PushToken) - - // Legacy push tokens that don't map to an app install are skipped - if pushTokenRecord.AppInstallId == nil { - continue - } - - // Only update the device with the latest app login for the owner - if *pushTokenRecord.AppInstallId != loginRecord.AppInstallId { - continue - } - - log.Debugf("updating badge count on device to %d", badgeCount) - - // Try push to update badge count - pushErr := pusher.SetAPNSBadgeCount( - ctx, - pushTokenRecord.PushToken, - badgeCount, - ) - - if pushErr != nil { - log.WithError(err).Warn("failure sending push notification") - isValid, err := onPushError(ctx, data, pusher, pushTokenRecord) - if isValid { - return errors.Wrap(pushErr, "error pushing to valid token") - } else if err != nil { - return errors.Wrap(err, "error handling push error") - } - } - } - - seenPushTokens[pushTokenRecord.PushToken] = struct{}{} - } - - return nil -} diff --git a/pkg/code/push/data.go b/pkg/code/push/data.go deleted file mode 100644 index af7ec041..00000000 --- a/pkg/code/push/data.go +++ /dev/null @@ -1,140 +0,0 @@ -package push - -import ( - "context" - - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - push_data "github.com/code-payments/code-server/pkg/code/data/push" - push_lib "github.com/code-payments/code-server/pkg/push" -) - -type dataPushType string - -const ( - dataPushTypeKey = "code_notification_type" - - chatMessageDataPush dataPushType = "ChatMessage" - executeSwapDataPush dataPushType = "ExecuteSwap" -) - -// sendRawDataPushNotificationToOwner is a generic utility for sending raw data push -// notification to the devices linked to an owner account. -// -// todo: Duplicated code with other send push utitilies -func sendRawDataPushNotificationToOwner( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - owner *common.Account, - notificationType dataPushType, - kvs map[string]string, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "sendRawDataPushNotificationToOwner", - "owner": owner.PublicKey().ToBase58(), - }) - - kvs[dataPushTypeKey] = string(notificationType) - - pushTokenRecords, err := getPushTokensForOwner(ctx, data, owner) - if err != nil { - log.WithError(err).Warn("failure getting push tokens for owner") - return err - } - - seenPushTokens := make(map[string]struct{}) - for _, pushTokenRecord := range pushTokenRecords { - // Dedup push tokens, since they may appear more than once per app install - if _, ok := seenPushTokens[pushTokenRecord.PushToken]; ok { - continue - } - - log := log.WithField("push_token", pushTokenRecord.PushToken) - - // Try push - err := pusher.SendDataPush( - ctx, - pushTokenRecord.PushToken, - kvs, - ) - - if err != nil { - log.WithError(err).Warn("failure sending push notification") - onPushError(ctx, data, pusher, pushTokenRecord) - } - - seenPushTokens[pushTokenRecord.PushToken] = struct{}{} - } - return nil -} - -// sendMutableNotificationToOwner is a generic utility for sending mutable -// push notification to the devices linked to an owner account. It's a -// special data push where the notification content is replaced by the contents -// of a kv pair payload. -// -// todo: Duplicated code with other send push utitilies -func sendMutableNotificationToOwner( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - owner *common.Account, - notificationType dataPushType, - titleKey string, - kvs map[string]string, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "sendMutableNotificationToOwner", - "owner": owner.PublicKey().ToBase58(), - }) - - kvs[dataPushTypeKey] = string(notificationType) - - pushTokenRecords, err := getPushTokensForOwner(ctx, data, owner) - if err != nil { - log.WithError(err).Warn("failure getting push tokens for owner") - return err - } - - seenPushTokens := make(map[string]struct{}) - for _, pushTokenRecord := range pushTokenRecords { - // Dedup push tokens, since they may appear more than once per app install - if _, ok := seenPushTokens[pushTokenRecord.PushToken]; ok { - continue - } - - log := log.WithField("push_token", pushTokenRecord.PushToken) - - // Try push - var err error - switch pushTokenRecord.TokenType { - case push_data.TokenTypeFcmApns: - err = pusher.SendMutableAPNSPush( - ctx, - pushTokenRecord.PushToken, - titleKey, - string(notificationType), - titleKey, // All mutable pushes have a thread ID that's the title - kvs, - ) - case push_data.TokenTypeFcmAndroid: - // todo: anything special required for Android? - err = pusher.SendDataPush( - ctx, - pushTokenRecord.PushToken, - kvs, - ) - } - - if err != nil { - log.WithError(err).Warn("failure sending push notification") - onPushError(ctx, data, pusher, pushTokenRecord) - } - - seenPushTokens[pushTokenRecord.PushToken] = struct{}{} - } - return nil -} diff --git a/pkg/code/push/notifications.go b/pkg/code/push/notifications.go deleted file mode 100644 index 42aff9be..00000000 --- a/pkg/code/push/notifications.go +++ /dev/null @@ -1,405 +0,0 @@ -package push - -import ( - "context" - "encoding/base64" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "google.golang.org/protobuf/proto" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - - chat_util "github.com/code-payments/code-server/pkg/code/chat" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/localization" - "github.com/code-payments/code-server/pkg/code/thirdparty" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - push_lib "github.com/code-payments/code-server/pkg/push" -) - -// SendDepositPushNotification sends a push notification for received deposits -func SendDepositPushNotification( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - vault *common.Account, - quarks uint64, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "SendDepositPushNotification", - "vault": vault.PublicKey().ToBase58(), - "quarks": quarks, - }) - - if quarks < kin.ToQuarks(1) { - return nil - } - - accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, vault.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting account info record") - return errors.Wrap(err, "error getting account info record") - } - - if accountInfoRecord.AccountType != commonpb.AccountType_PRIMARY { - return nil - } - - owner, err := common.NewAccountFromPublicKeyString(accountInfoRecord.OwnerAccount) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return errors.Wrap(err, "invalid owner account") - } - - // Legacy push notification still considers chat mute state - // - // todo: Proper migration to chat system - chatRecord, err := data.GetChatById(ctx, chat.GetChatId(chat_util.CashTransactionsName, owner.PublicKey().ToBase58(), true)) - switch err { - case nil: - if chatRecord.IsMuted { - return nil - } - case chat.ErrChatNotFound: - default: - log.WithError(err).Warn("failure getting chat record") - return errors.Wrap(err, "error getting chat record") - } - - locale, err := data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting user locale") - return err - } - - localizedAmount, err := localization.FormatFiat(locale, currency_lib.KIN, float64(kin.FromQuarks(quarks)), false) - if err != nil { - return nil - } - - localizedPushTitle, err := localization.Localize(locale, localization.PushTitleDepositReceived) - if err != nil { - return nil - } - - localizedPushBody, err := localization.Localize(locale, localization.PushSubtitleDepositReceived, localizedAmount) - if err != nil { - return nil - } - - return sendBasicPushNotificationToOwner( - ctx, - data, - pusher, - owner, - localizedPushTitle, - localizedPushBody, - ) -} - -// SendGiftCardReturnedPushNotification sends a push notification when a gift card -// has expired and the balance has returned to the issuing user -func SendGiftCardReturnedPushNotification( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - giftCardVault *common.Account, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "SendGiftCardReturnedPushNotification", - "vault": giftCardVault.PublicKey().ToBase58(), - }) - - accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, giftCardVault.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting account info record") - return errors.Wrap(err, "error getting account info record") - } - - if accountInfoRecord.AccountType != commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - return nil - } - - originalGiftCardIssuedIntent, err := data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVault.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting original gift card issued intent") - return errors.Wrap(err, "error getting original gift card issued intent") - } - - owner, err := common.NewAccountFromPublicKeyString(originalGiftCardIssuedIntent.InitiatorOwnerAccount) - if err != nil { - return errors.Wrap(err, "invalid owner") - } - - // Legacy push notification still considers chat mute state - // - // todo: Proper migration to chat system - chatRecord, err := data.GetChatById(ctx, chat.GetChatId(chat_util.CashTransactionsName, owner.PublicKey().ToBase58(), true)) - switch err { - case nil: - if chatRecord.IsMuted { - return nil - } - case chat.ErrChatNotFound: - default: - log.WithError(err).Warn("failure getting chat record") - return errors.Wrap(err, "error getting chat record") - } - - locale, err := data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting user locale") - return err - } - - localizedAmount, err := localization.FormatFiat( - locale, - originalGiftCardIssuedIntent.SendPrivatePaymentMetadata.ExchangeCurrency, - originalGiftCardIssuedIntent.SendPrivatePaymentMetadata.NativeAmount, - true, - ) - if err != nil { - return nil - } - - localizedPushTitle, err := localization.Localize(locale, localization.PushTitleKinReturned) - if err != nil { - return nil - } - - localizedPushBody, err := localization.Localize(locale, localization.PushSubtitleKinReturned, localizedAmount) - if err != nil { - return nil - } - - return sendBasicPushNotificationToOwner( - ctx, - data, - pusher, - owner, - localizedPushTitle, - localizedPushBody, - ) -} - -// SendTwitterAccountConnectedPushNotification sends a push notification for newly -// connected Twitter accounts -func SendTwitterAccountConnectedPushNotification( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - tipAccount *common.Account, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "SendTwitterAccountConnectedPushNotification", - "tip_account": tipAccount.PublicKey().ToBase58(), - }) - - accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, tipAccount.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting account info record") - return errors.Wrap(err, "error getting account info record") - } - if accountInfoRecord.AccountType != commonpb.AccountType_PRIMARY { - return nil - } - - owner, err := common.NewAccountFromPublicKeyString(accountInfoRecord.OwnerAccount) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return errors.Wrap(err, "invalid owner account") - } - - locale, err := data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting user locale") - return err - } - - localizedPushTitle, err := localization.Localize(locale, localization.PushTitleTwitterAccountConnected) - if err != nil { - return nil - } - - localizedPushBody, err := localization.Localize(locale, localization.PushSubtitleTwitterAccountConnected) - if err != nil { - return nil - } - - return sendBasicPushNotificationToOwner( - ctx, - data, - pusher, - owner, - localizedPushTitle, - localizedPushBody, - ) -} - -// SendTriggerSwapRpcPushNotification sends a data push to trigger the Swap RPC -func SendTriggerSwapRpcPushNotification( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - owner *common.Account, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "SendTriggerSwapRpcPushNotification", - "owner": owner.PublicKey().ToBase58(), - }) - - err := sendRawDataPushNotificationToOwner( - ctx, - data, - pusher, - owner, - executeSwapDataPush, - make(map[string]string), - ) - if err != nil { - log.WithError(err).Warn("failure sending data push notification") - return err - } - - return nil -} - -// SendChatMessagePushNotification sends a push notification for chat messages -func SendChatMessagePushNotification( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - chatTitle string, - owner *common.Account, - chatMessage *chatpb.ChatMessage, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "SendChatMessagePushNotification", - "owner": owner.PublicKey().ToBase58(), - "chat": chatTitle, - }) - - // Best-effort try to update the badge count before pushing message content - // - // Note: Only chat messages generate badge counts - err := UpdateBadgeCount(ctx, data, pusher, owner) - if err != nil { - log.WithError(err).Warn("failure updating badge count on device") - } - - locale, err := data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting user locale") - return err - } - - var localizedPushTitle string - - chatProperties, ok := chat_util.InternalChatProperties[chatTitle] - if ok { - localized, err := localization.Localize(locale, chatProperties.TitleLocalizationKey) - if err != nil { - return nil - } - localizedPushTitle = localized - } else { - domainDisplayName, err := thirdparty.GetDomainDisplayName(chatTitle) - if err == nil { - localizedPushTitle = domainDisplayName - } else { - return nil - } - } - - var anyErrorPushingContent bool - for _, content := range chatMessage.Content { - var contentToPush *chatpb.Content - switch typedContent := content.Type.(type) { - case *chatpb.Content_ServerLocalized: - localizedPushBody, err := localization.Localize(locale, typedContent.ServerLocalized.KeyOrText) - if err != nil { - continue - } - - contentToPush = &chatpb.Content{ - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: localizedPushBody, - }, - }, - } - case *chatpb.Content_ExchangeData: - var currencyCode currency_lib.Code - var nativeAmount float64 - if typedContent.ExchangeData.GetExact() != nil { - exchangeData := typedContent.ExchangeData.GetExact() - currencyCode = currency_lib.Code(exchangeData.Currency) - nativeAmount = exchangeData.NativeAmount - } else { - exchangeData := typedContent.ExchangeData.GetPartial() - currencyCode = currency_lib.Code(exchangeData.Currency) - nativeAmount = exchangeData.NativeAmount - } - - localizedPushBody, err := localization.LocalizeFiatWithVerb( - locale, - typedContent.ExchangeData.Verb, - currencyCode, - nativeAmount, - true, - ) - if err != nil { - continue - } - - contentToPush = &chatpb.Content{ - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: localizedPushBody, - }, - }, - } - case *chatpb.Content_NaclBox: - contentToPush = content - } - - if contentToPush == nil { - continue - } - - marshalledContent, err := proto.Marshal(contentToPush) - if err != nil { - log.WithError(err).Warn("failure marshalling chat content") - return err - } - - kvs := map[string]string{ - "chat_title": localizedPushTitle, - "message_content": base64.StdEncoding.EncodeToString(marshalledContent), - } - - err = sendMutableNotificationToOwner( - ctx, - data, - pusher, - owner, - chatMessageDataPush, - chatTitle, - kvs, - ) - if err != nil { - anyErrorPushingContent = true - log.WithError(err).Warn("failure sending data push notification") - } - } - - if anyErrorPushingContent { - return errors.New("at least one piece of content failed to push") - } - return nil -} diff --git a/pkg/code/push/text.go b/pkg/code/push/text.go deleted file mode 100644 index 80930bce..00000000 --- a/pkg/code/push/text.go +++ /dev/null @@ -1,60 +0,0 @@ -package push - -import ( - "context" - - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - push_lib "github.com/code-payments/code-server/pkg/push" -) - -// sendBasicPushNotificationToOwner is a generic utility for sending push notification -// to the devices linked to an owner account. -// -// todo: Duplicated code with other send push utitilies -func sendBasicPushNotificationToOwner( - ctx context.Context, - data code_data.Provider, - pusher push_lib.Provider, - owner *common.Account, - title, body string, -) error { - log := logrus.StandardLogger().WithFields(logrus.Fields{ - "method": "sendPushNotificationToOwner", - "owner": owner.PublicKey().ToBase58(), - }) - - pushTokenRecords, err := getPushTokensForOwner(ctx, data, owner) - if err != nil { - log.WithError(err).Warn("failure getting push tokens for owner") - return err - } - - seenPushTokens := make(map[string]struct{}) - for _, pushTokenRecord := range pushTokenRecords { - // Dedup push tokens, since they may appear more than once per app install - if _, ok := seenPushTokens[pushTokenRecord.PushToken]; ok { - continue - } - - log := log.WithField("push_token", pushTokenRecord.PushToken) - - // Try push - err := pusher.SendPush( - ctx, - pushTokenRecord.PushToken, - title, - body, - ) - - if err != nil { - log.WithError(err).Warn("failure sending push notification") - onPushError(ctx, data, pusher, pushTokenRecord) - } - - seenPushTokens[pushTokenRecord.PushToken] = struct{}{} - } - return nil -} diff --git a/pkg/code/push/util.go b/pkg/code/push/util.go deleted file mode 100644 index 55e33556..00000000 --- a/pkg/code/push/util.go +++ /dev/null @@ -1,41 +0,0 @@ -package push - -import ( - "context" - - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - push_data "github.com/code-payments/code-server/pkg/code/data/push" - push_lib "github.com/code-payments/code-server/pkg/push" -) - -func getPushTokensForOwner(ctx context.Context, data code_data.Provider, owner *common.Account) ([]*push_data.Record, error) { - verificationRecord, err := data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err != nil { - return nil, errors.Wrap(err, "error getting latest phone verification record") - } - - dataContainerRecord, err := data.GetUserDataContainerByPhone(ctx, owner.PublicKey().ToBase58(), verificationRecord.PhoneNumber) - if err != nil { - return nil, errors.Wrap(err, "error getting data container record") - } - - pushTokenRecords, err := data.GetAllValidPushTokensdByDataContainer(ctx, dataContainerRecord.ID) - if err == push_data.ErrTokenNotFound { - return nil, nil - } else if err != nil { - return nil, errors.Wrap(err, "error getting push token records") - } - return pushTokenRecords, nil -} - -func onPushError(ctx context.Context, data code_data.Provider, pusher push_lib.Provider, pushTokenRecord *push_data.Record) (bool, error) { - // On failure, verify token validity, and cleanup if necessary - isValid, err := pusher.IsValidPushToken(ctx, pushTokenRecord.PushToken) - if err == nil && !isValid { - data.DeletePushToken(ctx, pushTokenRecord.PushToken) - } - return isValid, err -} diff --git a/pkg/code/server/grpc/account/server.go b/pkg/code/server/account/server.go similarity index 96% rename from pkg/code/server/grpc/account/server.go rename to pkg/code/server/account/server.go index 87cf949c..bf31bed7 100644 --- a/pkg/code/server/grpc/account/server.go +++ b/pkg/code/server/account/server.go @@ -2,7 +2,6 @@ package account import ( "context" - "errors" "time" "github.com/sirupsen/logrus" @@ -23,7 +22,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/action" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/kin" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) @@ -499,23 +497,14 @@ func (s *server) getProtoAccountInfo(ctx context.Context, records *common.Accoun } var originalExchangeData *transactionpb.ExchangeData - if records.General.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - originalExchangeData, err = s.getOriginalGiftCardExchangeData(ctx, records) - if err != nil { - return nil, err - } - } - - var relationship *commonpb.Relationship - if records.General.AccountType == commonpb.AccountType_RELATIONSHIP { - relationship = &commonpb.Relationship{ - Type: &commonpb.Relationship_Domain{ - Domain: &commonpb.Domain{ - Value: *records.General.RelationshipTo, - }, - }, + /* + if records.General.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { + originalExchangeData, err = s.getOriginalGiftCardExchangeData(ctx, records) + if err != nil { + return nil, err + } } - } + */ return &accountpb.TokenAccountInfo{ Address: tokenAccount.ToProto(), @@ -530,7 +519,6 @@ func (s *server) getProtoAccountInfo(ctx context.Context, records *common.Accoun MustRotate: mustRotate, ClaimState: claimState, OriginalExchangeData: originalExchangeData, - Relationship: relationship, Mint: mintAccount.ToProto(), CreatedAt: timestamppb.New(records.General.CreatedAt), }, nil @@ -546,6 +534,7 @@ func (s *server) shouldClientRotateAccount(ctx context.Context, records *common. return balance > 0, nil } +/* func (s *server) getOriginalGiftCardExchangeData(ctx context.Context, records *common.AccountRecords) (*transactionpb.ExchangeData, error) { if records.General.AccountType != commonpb.AccountType_REMOTE_SEND_GIFT_CARD { return nil, errors.New("invalid account type") @@ -563,7 +552,4 @@ func (s *server) getOriginalGiftCardExchangeData(ctx context.Context, records *c Quarks: intentRecord.SendPrivatePaymentMetadata.Quantity, }, nil } - -func hideDust(quarks uint64) uint64 { - return kin.ToQuarks(kin.FromQuarks(quarks)) -} +*/ diff --git a/pkg/code/server/grpc/account/server_test.go b/pkg/code/server/account/server_test.go similarity index 92% rename from pkg/code/server/grpc/account/server_test.go rename to pkg/code/server/account/server_test.go index 1852ee8c..effbeb60 100644 --- a/pkg/code/server/grpc/account/server_test.go +++ b/pkg/code/server/account/server_test.go @@ -25,11 +25,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/code/data/transaction" - "github.com/code-payments/code-server/pkg/code/data/user" - user_identity "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/testutil" ) @@ -155,20 +150,14 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { bucketDerivedOwner := testutil.NewRandomAccount(t) tempIncomingDerivedOwner := testutil.NewRandomAccount(t) - relationship1DerivedOwner := testutil.NewRandomAccount(t) - relationship2DerivedOwner := testutil.NewRandomAccount(t) swapDerivedOwner := testutil.NewRandomAccount(t) primaryAccountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_PRIMARY) bucketAccountRecords := setupAccountRecords(t, env, ownerAccount, bucketDerivedOwner, 0, commonpb.AccountType_BUCKET_100_KIN) - relationship1AccountRecords := setupAccountRecords(t, env, ownerAccount, relationship1DerivedOwner, 0, commonpb.AccountType_RELATIONSHIP) - relationship2AccountRecords := setupAccountRecords(t, env, ownerAccount, relationship2DerivedOwner, 0, commonpb.AccountType_RELATIONSHIP) setupAccountRecords(t, env, ownerAccount, swapDerivedOwner, 0, commonpb.AccountType_SWAP) setupAccountRecords(t, env, ownerAccount, tempIncomingDerivedOwner, 2, commonpb.AccountType_TEMPORARY_INCOMING) - setupCachedBalance(t, env, bucketAccountRecords, kin.ToQuarks(100)) - setupCachedBalance(t, env, primaryAccountRecords, kin.ToQuarks(42)) - setupCachedBalance(t, env, relationship1AccountRecords, kin.ToQuarks(999)) - setupCachedBalance(t, env, relationship2AccountRecords, kin.ToQuarks(5)) + setupCachedBalance(t, env, bucketAccountRecords, common.ToCoreMintQuarks(100)) + setupCachedBalance(t, env, primaryAccountRecords, common.ToCoreMintQuarks(42)) otherOwnerAccount := testutil.NewRandomAccount(t) setupAccountRecords(t, env, otherOwnerAccount, otherOwnerAccount, 0, commonpb.AccountType_PRIMARY) @@ -178,14 +167,12 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { resp, err := env.client.GetTokenAccountInfos(env.ctx, req) require.NoError(t, err) assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 6) + assert.Len(t, resp.TokenAccountInfos, 4) for _, authority := range []*common.Account{ ownerAccount, bucketDerivedOwner, tempIncomingDerivedOwner, - relationship1DerivedOwner, - relationship2DerivedOwner, swapDerivedOwner, } { var tokenAccount *common.Account @@ -193,7 +180,7 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { tokenAccount, err = authority.ToAssociatedTokenAccount(common.UsdcMintAccount) require.NoError(t, err) } else { - timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) require.NoError(t, err) tokenAccount = timelockAccounts.Vault } @@ -209,27 +196,15 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { case ownerAccount.PublicKey().ToBase58(): assert.Equal(t, commonpb.AccountType_PRIMARY, accountInfo.AccountType) assert.EqualValues(t, 0, accountInfo.Index) - assert.EqualValues(t, kin.ToQuarks(42), accountInfo.Balance) + assert.EqualValues(t, common.ToCoreMintQuarks(42), accountInfo.Balance) case bucketDerivedOwner.PublicKey().ToBase58(): assert.Equal(t, commonpb.AccountType_BUCKET_100_KIN, accountInfo.AccountType) assert.EqualValues(t, 0, accountInfo.Index) - assert.EqualValues(t, kin.ToQuarks(100), accountInfo.Balance) + assert.EqualValues(t, common.ToCoreMintQuarks(100), accountInfo.Balance) case tempIncomingDerivedOwner.PublicKey().ToBase58(): assert.Equal(t, commonpb.AccountType_TEMPORARY_INCOMING, accountInfo.AccountType) assert.EqualValues(t, 2, accountInfo.Index) assert.EqualValues(t, 0, accountInfo.Balance) - case relationship1DerivedOwner.PublicKey().ToBase58(): - assert.Equal(t, commonpb.AccountType_RELATIONSHIP, accountInfo.AccountType) - assert.EqualValues(t, 0, accountInfo.Index) - assert.EqualValues(t, kin.ToQuarks(999), accountInfo.Balance) - require.NotNil(t, accountInfo.Relationship) - assert.Equal(t, *relationship1AccountRecords.General.RelationshipTo, accountInfo.Relationship.GetDomain().Value) - case relationship2DerivedOwner.PublicKey().ToBase58(): - assert.Equal(t, commonpb.AccountType_RELATIONSHIP, accountInfo.AccountType) - assert.EqualValues(t, 0, accountInfo.Index) - assert.EqualValues(t, kin.ToQuarks(5), accountInfo.Balance) - require.NotNil(t, accountInfo.Relationship) - assert.Equal(t, *relationship2AccountRecords.General.RelationshipTo, accountInfo.Relationship.GetDomain().Value) case swapDerivedOwner.PublicKey().ToBase58(): assert.Equal(t, commonpb.AccountType_SWAP, accountInfo.AccountType) assert.EqualValues(t, 0, accountInfo.Index) @@ -238,10 +213,6 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { require.Fail(t, "unexpected authority") } - if accountInfo.AccountType != commonpb.AccountType_RELATIONSHIP { - assert.Nil(t, accountInfo.Relationship) - } - if accountInfo.AccountType == commonpb.AccountType_SWAP { assert.Equal(t, accountpb.TokenAccountInfo_BALANCE_SOURCE_BLOCKCHAIN, accountInfo.BalanceSource) assert.Equal(t, accountpb.TokenAccountInfo_MANAGEMENT_STATE_NONE, accountInfo.ManagementState) @@ -251,7 +222,7 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { assert.Equal(t, accountpb.TokenAccountInfo_BALANCE_SOURCE_CACHE, accountInfo.BalanceSource) assert.Equal(t, accountpb.TokenAccountInfo_MANAGEMENT_STATE_LOCKED, accountInfo.ManagementState) assert.Equal(t, accountpb.TokenAccountInfo_BLOCKCHAIN_STATE_EXISTS, accountInfo.BlockchainState) - assert.Equal(t, common.KinMintAccount.PublicKey().ToBytes(), accountInfo.Mint.Value) + assert.Equal(t, common.CoreMintAccount.PublicKey().ToBytes(), accountInfo.Mint.Value) } assert.False(t, accountInfo.MustRotate) @@ -264,6 +235,7 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { assert.True(t, primaryAccountInfoRecord.RequiresDepositSync) } +/* func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { env, cleanup := setup(t) defer cleanup() @@ -271,7 +243,7 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { // Test cases represent main iterations of a gift card account's state throughout // its lifecycle. All states beyond claimed status are not fully tested here and // are done elsewhere. - for i, tc := range []struct { + for _, tc := range []struct { balance uint64 timelockState timelock_token_v1.TimelockState simulateClaimInCode bool @@ -392,9 +364,8 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { expectedClaimState: accountpb.TokenAccountInfo_CLAIM_STATE_EXPIRED, }, } { - phoneNumber := fmt.Sprintf("+1800555%d", i) ownerAccount := testutil.NewRandomAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) require.NoError(t, err) req := &accountpb.GetTokenAccountInfosRequest{ @@ -406,15 +377,6 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), } - userIdentityRecord := &user_identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUser(env.ctx, userIdentityRecord)) - accountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, 0, commonpb.AccountType_REMOTE_SEND_GIFT_CARD) giftCardIssuedIntentRecord := &intent.Record{ @@ -422,11 +384,10 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { IntentType: intent.SendPrivatePayment, InitiatorOwnerAccount: testutil.NewRandomAccount(t).PrivateKey().ToBase58(), - InitiatorPhoneNumber: &phoneNumber, SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ DestinationTokenAccount: accountRecords.General.TokenAccount, - Quantity: kin.ToQuarks(10), + Quantity: common.ToCoreMintQuarks(10), ExchangeCurrency: currency.CAD, ExchangeRate: 1.23, @@ -456,8 +417,6 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { Destination: pointer.String("primary"), Quantity: nil, - InitiatorPhoneNumber: &phoneNumber, - State: action.StateUnknown, } if tc.simulateAutoReturnInCode { @@ -477,8 +436,6 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { Destination: pointer.String("destination"), Quantity: pointer.Uint64(tc.balance - 1), // Explicitly less than the actual balance - InitiatorPhoneNumber: &phoneNumber, - State: action.StatePending, } require.NoError(t, env.data.PutAllActions(env.ctx, claimActionRecord)) @@ -500,7 +457,7 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { assert.Equal(t, ownerAccount.PublicKey().ToBytes(), accountInfo.Owner.Value) assert.Equal(t, ownerAccount.PublicKey().ToBytes(), accountInfo.Authority.Value) assert.Equal(t, timelockAccounts.Vault.PublicKey().ToBytes(), accountInfo.Address.Value) - assert.Equal(t, common.KinMintAccount.PublicKey().ToBytes(), accountInfo.Mint.Value) + assert.Equal(t, common.CoreMintAccount.PublicKey().ToBytes(), accountInfo.Mint.Value) assert.EqualValues(t, 0, accountInfo.Index) assert.Equal(t, tc.expectedBalanceSource, accountInfo.BalanceSource) @@ -524,13 +481,12 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { assert.False(t, accountInfo.MustRotate) - assert.Nil(t, accountInfo.Relationship) - accountInfoRecord, err := env.data.GetLatestAccountInfoByOwnerAddressAndType(env.ctx, ownerAccount.PublicKey().ToBase58(), commonpb.AccountType_REMOTE_SEND_GIFT_CARD) require.NoError(t, err) assert.False(t, accountInfoRecord.RequiresDepositSync) } } +*/ func TestGetTokenAccountInfos_BlockchainState(t *testing.T) { env, cleanup := setup(t) @@ -803,7 +759,7 @@ func TestLinkAdditionalAccounts_InvalidSwapAuthority(t *testing.T) { OwnerAccount: ownerAccount.PublicKey().ToBase58(), AuthorityAccount: swapAuthorityAccount.PublicKey().ToBase58(), TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_BUCKET_100_000_KIN, })) }, @@ -947,7 +903,7 @@ func getDefaultTestAccountRecords(t *testing.T, env testEnv, ownerAccount, autho tokenAccount, err = authorityAccount.ToAssociatedTokenAccount(mintAccount) require.NoError(t, err) } else { - mintAccount = common.KinMintAccount + mintAccount = common.CoreMintAccount timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, mintAccount) require.NoError(t, err) @@ -967,10 +923,6 @@ func getDefaultTestAccountRecords(t *testing.T, env testEnv, ownerAccount, autho Index: index, } - if accountInfoRecord.AccountType == commonpb.AccountType_RELATIONSHIP { - accountInfoRecord.RelationshipTo = pointer.String(fmt.Sprintf("app%d.com", rand.Int())) - } - return &common.AccountRecords{ General: accountInfoRecord, Timelock: timelockRecord, diff --git a/pkg/code/server/grpc/currency/currency.go b/pkg/code/server/currency/currency.go similarity index 100% rename from pkg/code/server/grpc/currency/currency.go rename to pkg/code/server/currency/currency.go diff --git a/pkg/code/server/grpc/badge/server.go b/pkg/code/server/grpc/badge/server.go deleted file mode 100644 index e762e117..00000000 --- a/pkg/code/server/grpc/badge/server.go +++ /dev/null @@ -1,70 +0,0 @@ -package badge - -import ( - "context" - - "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - badgepb "github.com/code-payments/code-protobuf-api/generated/go/badge/v1" - - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/push" - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - push_util "github.com/code-payments/code-server/pkg/code/push" -) - -type server struct { - log *logrus.Entry - data code_data.Provider - auth *auth_util.RPCSignatureVerifier - pushProvider push.Provider - - badgepb.UnimplementedBadgeServer -} - -func NewBadgeServer(data code_data.Provider, pushProvider push.Provider, auth *auth_util.RPCSignatureVerifier) badgepb.BadgeServer { - return &server{ - log: logrus.StandardLogger().WithField("type", "badge/server"), - data: data, - auth: auth, - pushProvider: pushProvider, - } -} - -func (s *server) ResetBadgeCount(ctx context.Context, req *badgepb.ResetBadgeCountRequest) (*badgepb.ResetBadgeCountResponse, error) { - log := s.log.WithField("method", "ResetBadgeCount") - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - err = s.data.ResetBadgeCount(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure resetting db badge count") - return nil, status.Error(codes.Internal, "") - } - - err = push_util.UpdateBadgeCount(ctx, s.data, s.pushProvider, owner) - if err != nil { - log.WithError(err).Warn("failure updating badge count on device") - return nil, status.Error(codes.Internal, "") - } - - return &badgepb.ResetBadgeCountResponse{ - Result: badgepb.ResetBadgeCountResponse_OK, - }, nil -} diff --git a/pkg/code/server/grpc/badge/server_test.go b/pkg/code/server/grpc/badge/server_test.go deleted file mode 100644 index 70b02d30..00000000 --- a/pkg/code/server/grpc/badge/server_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package badge - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/protobuf/proto" - - badgepb "github.com/code-payments/code-protobuf-api/generated/go/badge/v1" - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - - memory_push "github.com/code-payments/code-server/pkg/push/memory" - "github.com/code-payments/code-server/pkg/testutil" - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/badgecount" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/push" - "github.com/code-payments/code-server/pkg/code/data/user" - user_identity "github.com/code-payments/code-server/pkg/code/data/user/identity" - user_storage "github.com/code-payments/code-server/pkg/code/data/user/storage" -) - -func TestResetBadgeCount_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - - env.createUser(t, owner, "+12223334444") - - req := &badgepb.ResetBadgeCountRequest{ - Owner: owner.ToProto(), - } - req.Signature = signProtoMessage(t, req, owner, false) - - resp, err := env.client.ResetBadgeCount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, resp.Result, badgepb.ResetBadgeCountResponse_OK) - env.assertBadgeCount(t, owner, 0) - - env.data.AddToBadgeCount(env.ctx, owner.PublicKey().ToBase58(), 5) - env.assertBadgeCount(t, owner, 5) - - resp, err = env.client.ResetBadgeCount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, resp.Result, badgepb.ResetBadgeCountResponse_OK) - env.assertBadgeCount(t, owner, 0) -} - -func TestUnauthorizedAccess(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - - resetReq := &badgepb.ResetBadgeCountRequest{ - Owner: owner.ToProto(), - } - resetReq.Signature = signProtoMessage(t, resetReq, owner, true) - - _, err := env.client.ResetBadgeCount(env.ctx, resetReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) -} - -type testEnv struct { - ctx context.Context - client badgepb.BadgeClient - server *server - data code_data.Provider -} - -func setup(t *testing.T) (env *testEnv, cleanup func()) { - conn, serv, err := testutil.NewServer() - require.NoError(t, err) - - env = &testEnv{ - ctx: context.Background(), - client: badgepb.NewBadgeClient(conn), - data: code_data.NewTestDataProvider(), - } - - s := NewBadgeServer(env.data, memory_push.NewPushProvider(), auth_util.NewRPCSignatureVerifier(env.data)) - env.server = s.(*server) - - serv.RegisterService(func(server *grpc.Server) { - badgepb.RegisterBadgeServer(server, s) - }) - - cleanup, err = serv.Serve() - require.NoError(t, err) - return env, cleanup -} - -func (e *testEnv) createUser(t *testing.T, owner *common.Account, phoneNumber string) { - phoneVerificationRecord := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: owner.PublicKey().ToBase58(), - LastVerifiedAt: time.Now(), - CreatedAt: time.Now(), - } - require.NoError(t, e.data.SavePhoneVerification(e.ctx, phoneVerificationRecord)) - - userIdentityRecord := &user_identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, e.data.PutUser(e.ctx, userIdentityRecord)) - - userStorageRecord := &user_storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: owner.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, e.data.PutUserDataContainer(e.ctx, userStorageRecord)) - - pushTokenRecord := push.Record{ - DataContainerId: *userStorageRecord.ID, - - PushToken: memory_push.ValidApplePushToken, - TokenType: push.TokenTypeFcmApns, - IsValid: true, - - CreatedAt: time.Now(), - } - require.NoError(t, e.data.PutPushToken(e.ctx, &pushTokenRecord)) -} - -func (e *testEnv) assertBadgeCount(t *testing.T, owner *common.Account, expected int) { - badgeCountRecord, err := e.data.GetBadgeCount(e.ctx, owner.PublicKey().ToBase58()) - if err == badgecount.ErrBadgeCountNotFound { - assert.Equal(t, 0, expected) - return - } - require.NoError(t, err) - assert.EqualValues(t, expected, badgeCountRecord.BadgeCount) -} - -func signProtoMessage(t *testing.T, msg proto.Message, signer *common.Account, simulateInvalidSignature bool) *commonpb.Signature { - msgBytes, err := proto.Marshal(msg) - require.NoError(t, err) - - if simulateInvalidSignature { - signer = testutil.NewRandomAccount(t) - } - - signature, err := signer.Sign(msgBytes) - require.NoError(t, err) - - return &commonpb.Signature{ - Value: signature, - } -} diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/server.go deleted file mode 100644 index 17c201ca..00000000 --- a/pkg/code/server/grpc/chat/server.go +++ /dev/null @@ -1,531 +0,0 @@ -package chat - -import ( - "context" - "math" - - "github.com/mr-tron/base58" - "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - - auth_util "github.com/code-payments/code-server/pkg/code/auth" - chat_util "github.com/code-payments/code-server/pkg/code/chat" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/localization" - "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/grpc/client" -) - -const ( - maxPageSize = 100 -) - -type server struct { - log *logrus.Entry - data code_data.Provider - auth *auth_util.RPCSignatureVerifier - - chatpb.UnimplementedChatServer -} - -func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier) chatpb.ChatServer { - return &server{ - log: logrus.StandardLogger().WithField("type", "chat/server"), - data: data, - auth: auth, - } -} - -func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*chatpb.GetChatsResponse, error) { - log := s.log.WithField("method", "GetChats") - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - var limit uint64 - if req.PageSize > 0 { - limit = uint64(req.PageSize) - } else { - limit = maxPageSize - } - if limit > maxPageSize { - limit = maxPageSize - } - - var direction query.Ordering - if req.Direction == chatpb.GetChatsRequest_ASC { - direction = query.Ascending - } else { - direction = query.Descending - } - - var cursor query.Cursor - if req.Cursor != nil { - cursor = req.Cursor.Value - } else { - cursor = query.ToCursor(0) - if direction == query.Descending { - cursor = query.ToCursor(math.MaxInt64 - 1) - } - } - - chatRecords, err := s.data.GetAllChatsForUser( - ctx, - owner.PublicKey().ToBase58(), - query.WithCursor(cursor), - query.WithDirection(direction), - query.WithLimit(limit), - ) - if err == chat.ErrChatNotFound { - return &chatpb.GetChatsResponse{ - Result: chatpb.GetChatsResponse_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting chat records") - return nil, status.Error(codes.Internal, "") - } - - locale, err := s.data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting user locale") - return nil, status.Error(codes.Internal, "") - } - - var protoMetadatas []*chatpb.ChatMetadata - for _, chatRecord := range chatRecords { - log := log.WithField("chat_id", chatRecord.ChatId.String()) - - protoMetadata := &chatpb.ChatMetadata{ - ChatId: chatRecord.ChatId.ToProto(), - IsVerified: chatRecord.IsVerified, - IsMuted: chatRecord.IsMuted, - IsSubscribed: !chatRecord.IsUnsubscribed, - CanMute: true, - CanUnsubscribe: true, - Cursor: &chatpb.Cursor{ - Value: query.ToCursor(chatRecord.Id), - }, - } - - // Unread count calculations can be skipped for unsubscribed chats. They - // don't appear in chat history. - skipUnreadCountQuery := chatRecord.IsUnsubscribed - - switch chatRecord.ChatType { - case chat.ChatTypeInternal: - chatProperties, ok := chat_util.InternalChatProperties[chatRecord.ChatTitle] - if !ok { - log.Warnf("%s chat doesn't have properties defined", chatRecord.ChatTitle) - continue - } - - // All messages for cash transactions and payments are silent, and this - // history may be long, so we can skip the unread count calculation as - // an optimization. - switch chatRecord.ChatTitle { - case chat_util.CashTransactionsName, chat_util.PaymentsName: - skipUnreadCountQuery = true - } - - protoMetadata.Title = &chatpb.ChatMetadata_Localized{ - Localized: &chatpb.ServerLocalizedContent{ - KeyOrText: localization.LocalizeWithFallback( - locale, - localization.GetLocalizationKeyForUserAgent(ctx, chatProperties.TitleLocalizationKey), - chatProperties.TitleLocalizationKey, - ), - }, - } - protoMetadata.CanMute = chatProperties.CanMute - protoMetadata.CanUnsubscribe = chatProperties.CanUnsubscribe - case chat.ChatTypeExternalApp: - protoMetadata.Title = &chatpb.ChatMetadata_Domain{ - Domain: &commonpb.Domain{ - Value: chatRecord.ChatTitle, - }, - } - - // All messages for unverified merchant chats are silent, and this history - // may be long, so we can skip the unread count calculation as an optimization. - skipUnreadCountQuery = skipUnreadCountQuery || !chatRecord.IsVerified - default: - log.WithField("chat_type", chatRecord.ChatType).Warn("unhandled chat type") - return nil, status.Error(codes.Internal, "") - } - - if chatRecord.ReadPointer != nil { - pointerValue, err := base58.Decode(*chatRecord.ReadPointer) - if err != nil { - log.WithError(err).Warn("failure decoding read pointer value") - return nil, status.Error(codes.Internal, "") - } - - protoMetadata.ReadPointer = &chatpb.Pointer{ - Kind: chatpb.Pointer_READ, - Value: &chatpb.ChatMessageId{ - Value: pointerValue, - }, - } - } - - if !skipUnreadCountQuery && !chatRecord.IsMuted && !chatRecord.IsUnsubscribed { - // todo: will need batching when users have a large number of chats - unreadCount, err := s.data.GetChatUnreadCount(ctx, chatRecord.ChatId) - if err != nil { - log.WithError(err).Warn("failure getting unread count") - } - protoMetadata.NumUnread = unreadCount - } - - protoMetadatas = append(protoMetadatas, protoMetadata) - - if len(protoMetadatas) >= maxPageSize { - break - } - } - - return &chatpb.GetChatsResponse{ - Result: chatpb.GetChatsResponse_OK, - Chats: protoMetadatas, - }, nil -} - -func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest) (*chatpb.GetMessagesResponse, error) { - log := s.log.WithField("method", "GetMessages") - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - chatId := chat.ChatIdFromProto(req.ChatId) - log = log.WithField("chat_id", chatId.String()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - chatRecord, err := s.data.GetChatById(ctx, chatId) - if err == chat.ErrChatNotFound { - return &chatpb.GetMessagesResponse{ - Result: chatpb.GetMessagesResponse_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting chat record") - return nil, status.Error(codes.Internal, "") - } - - if chatRecord.CodeUser != owner.PublicKey().ToBase58() { - return nil, status.Error(codes.PermissionDenied, "") - } - - var limit uint64 - if req.PageSize > 0 { - limit = uint64(req.PageSize) - } else { - limit = maxPageSize - } - if limit > maxPageSize { - limit = maxPageSize - } - - var direction query.Ordering - if req.Direction == chatpb.GetMessagesRequest_ASC { - direction = query.Ascending - } else { - direction = query.Descending - } - - var cursor query.Cursor - if req.Cursor != nil { - cursor = req.Cursor.Value - } - - messageRecords, err := s.data.GetAllChatMessages( - ctx, - chatId, - query.WithCursor(cursor), - query.WithDirection(direction), - query.WithLimit(limit), - ) - if err == chat.ErrMessageNotFound { - return &chatpb.GetMessagesResponse{ - Result: chatpb.GetMessagesResponse_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting chat message records") - return nil, status.Error(codes.Internal, "") - } - - locale, err := s.data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting user locale") - return nil, status.Error(codes.Internal, "") - } - - var protoChatMessages []*chatpb.ChatMessage - for _, messageRecord := range messageRecords { - var protoChatMessage chatpb.ChatMessage - err = proto.Unmarshal(messageRecord.Data, &protoChatMessage) - if err != nil { - log.WithError(err).Warn("failure unmarshalling proto chat message") - return nil, status.Error(codes.Internal, "") - } - - for _, content := range protoChatMessage.Content { - switch typed := content.Type.(type) { - case *chatpb.Content_ServerLocalized: - typed.ServerLocalized.KeyOrText = localization.LocalizeWithFallback( - locale, - localization.GetLocalizationKeyForUserAgent(ctx, typed.ServerLocalized.KeyOrText), - typed.ServerLocalized.KeyOrText, - ) - } - } - - messageIdBytes, err := base58.Decode(messageRecord.MessageId) - if err != nil { - log.WithError(err).Warn("failure decoding chat message id") - return nil, status.Error(codes.Internal, "") - } - - protoChatMessage.MessageId = &chatpb.ChatMessageId{ - Value: messageIdBytes, - } - protoChatMessage.Ts = timestamppb.New(messageRecord.Timestamp) - protoChatMessage.Cursor = &chatpb.Cursor{ - // Non-standard cursor because we index on time and don't have append - // semantics to the message log - Value: messageIdBytes, - } - - protoChatMessages = append(protoChatMessages, &protoChatMessage) - - if len(protoChatMessages) >= maxPageSize { - break - } - } - - return &chatpb.GetMessagesResponse{ - Result: chatpb.GetMessagesResponse_OK, - Messages: protoChatMessages, - }, nil -} - -func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerRequest) (*chatpb.AdvancePointerResponse, error) { - log := s.log.WithField("method", "AdvancePointer") - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - chatId := chat.ChatIdFromProto(req.ChatId) - log = log.WithField("chat_id", chatId.String()) - - messageId := base58.Encode(req.Pointer.Value.Value) - log = log.WithFields(logrus.Fields{ - "message_id": messageId, - "pointer_type": req.Pointer.Kind, - }) - - if req.Pointer.Kind != chatpb.Pointer_READ { - return nil, status.Error(codes.InvalidArgument, "Pointer.Kind must be READ") - } - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - chatRecord, err := s.data.GetChatById(ctx, chatId) - if err == chat.ErrChatNotFound { - return &chatpb.AdvancePointerResponse{ - Result: chatpb.AdvancePointerResponse_CHAT_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting chat record") - return nil, status.Error(codes.Internal, "") - } - - if chatRecord.CodeUser != owner.PublicKey().ToBase58() { - return nil, status.Error(codes.PermissionDenied, "") - } - - newPointerRecord, err := s.data.GetChatMessage(ctx, chatId, messageId) - if err == chat.ErrMessageNotFound { - return &chatpb.AdvancePointerResponse{ - Result: chatpb.AdvancePointerResponse_MESSAGE_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting chat message record for new pointer value") - return nil, status.Error(codes.Internal, "") - } - - if chatRecord.ReadPointer != nil { - oldPointerRecord, err := s.data.GetChatMessage(ctx, chatId, *chatRecord.ReadPointer) - if err != nil { - log.WithError(err).Warn("failure getting chat message record for old pointer value") - return nil, status.Error(codes.Internal, "") - } - - if oldPointerRecord.MessageId == newPointerRecord.MessageId || oldPointerRecord.Timestamp.After(newPointerRecord.Timestamp) { - return &chatpb.AdvancePointerResponse{ - Result: chatpb.AdvancePointerResponse_OK, - }, nil - } - } - - err = s.data.AdvanceChatPointer(ctx, chatId, messageId) - if err != nil { - log.WithError(err).Warn("failure advancing pointer") - return nil, status.Error(codes.Internal, "") - } - return &chatpb.AdvancePointerResponse{ - Result: chatpb.AdvancePointerResponse_OK, - }, nil -} - -func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateRequest) (*chatpb.SetMuteStateResponse, error) { - log := s.log.WithField("method", "SetMuteState") - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - chatId := chat.ChatIdFromProto(req.ChatId) - log = log.WithField("chat_id", chatId.String()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - chatRecord, err := s.data.GetChatById(ctx, chatId) - if err == chat.ErrChatNotFound { - return &chatpb.SetMuteStateResponse{ - Result: chatpb.SetMuteStateResponse_CHAT_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting chat record") - return nil, status.Error(codes.Internal, "") - } - - if chatRecord.CodeUser != owner.PublicKey().ToBase58() { - return nil, status.Error(codes.PermissionDenied, "") - } - - if chatRecord.IsMuted == req.IsMuted { - return &chatpb.SetMuteStateResponse{ - Result: chatpb.SetMuteStateResponse_OK, - }, nil - } - - chatProperties, ok := chat_util.InternalChatProperties[chatRecord.ChatTitle] - if ok && req.IsMuted && !chatProperties.CanMute { - return &chatpb.SetMuteStateResponse{ - Result: chatpb.SetMuteStateResponse_CANT_MUTE, - }, nil - } - - err = s.data.SetChatMuteState(ctx, chatId, req.IsMuted) - if err != nil { - log.WithError(err).Warn("failure setting mute status") - return nil, status.Error(codes.Internal, "") - } - - return &chatpb.SetMuteStateResponse{ - Result: chatpb.SetMuteStateResponse_OK, - }, nil -} - -func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscriptionStateRequest) (*chatpb.SetSubscriptionStateResponse, error) { - log := s.log.WithField("method", "SetSubscriptionState") - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - chatId := chat.ChatIdFromProto(req.ChatId) - log = log.WithField("chat_id", chatId.String()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - chatRecord, err := s.data.GetChatById(ctx, chatId) - if err == chat.ErrChatNotFound { - return &chatpb.SetSubscriptionStateResponse{ - Result: chatpb.SetSubscriptionStateResponse_CHAT_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting chat record") - return nil, status.Error(codes.Internal, "") - } - - if chatRecord.CodeUser != owner.PublicKey().ToBase58() { - return nil, status.Error(codes.PermissionDenied, "") - } - - if chatRecord.IsUnsubscribed != req.IsSubscribed { - return &chatpb.SetSubscriptionStateResponse{ - Result: chatpb.SetSubscriptionStateResponse_OK, - }, nil - } - - chatProperties, ok := chat_util.InternalChatProperties[chatRecord.ChatTitle] - if ok && !req.IsSubscribed && !chatProperties.CanUnsubscribe { - return &chatpb.SetSubscriptionStateResponse{ - Result: chatpb.SetSubscriptionStateResponse_CANT_UNSUBSCRIBE, - }, nil - } - - err = s.data.SetChatSubscriptionState(ctx, chatId, req.IsSubscribed) - if err != nil { - log.WithError(err).Warn("failure setting subcription status") - return nil, status.Error(codes.Internal, "") - } - - return &chatpb.SetSubscriptionStateResponse{ - Result: chatpb.SetSubscriptionStateResponse_OK, - }, nil -} diff --git a/pkg/code/server/grpc/chat/server_test.go b/pkg/code/server/grpc/chat/server_test.go deleted file mode 100644 index 627764b0..00000000 --- a/pkg/code/server/grpc/chat/server_test.go +++ /dev/null @@ -1,946 +0,0 @@ -package chat - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/golang/protobuf/proto" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/text/language" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/protobuf/types/known/timestamppb" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - auth_util "github.com/code-payments/code-server/pkg/code/auth" - chat_util "github.com/code-payments/code-server/pkg/code/chat" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/storage" - "github.com/code-payments/code-server/pkg/code/localization" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/testutil" -) - -// todo: This could use a refactor with some testing utilities - -func TestGetChatsAndMessages_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - localization.LoadTestKeys(map[language.Tag]map[string]string{ - language.English: { - localization.ChatTitleCodeTeam: "Code Team", - "msg.body.key": "localized message body content", - }, - }) - defer localization.ResetKeys() - - testExternalAppDomain := "test.com" - - cashTransactionsChatId := chat.GetChatId(chat_util.CashTransactionsName, owner.PublicKey().ToBase58(), true) - codeTeamChatId := chat.GetChatId(chat_util.CodeTeamName, owner.PublicKey().ToBase58(), true) - verifiedExternalAppChatId := chat.GetChatId(testExternalAppDomain, owner.PublicKey().ToBase58(), true) - unverifiedExternalAppChatId := chat.GetChatId(testExternalAppDomain, owner.PublicKey().ToBase58(), false) - - getChatsReq := &chatpb.GetChatsRequest{ - Owner: owner.ToProto(), - } - getChatsReq.Signature = signProtoMessage(t, getChatsReq, owner, false) - - getCodeTeamMessagesReq := &chatpb.GetMessagesRequest{ - ChatId: codeTeamChatId.ToProto(), - Owner: owner.ToProto(), - } - getCodeTeamMessagesReq.Signature = signProtoMessage(t, getCodeTeamMessagesReq, owner, false) - - getCashTransactionsMessagesReq := &chatpb.GetMessagesRequest{ - ChatId: cashTransactionsChatId.ToProto(), - Owner: owner.ToProto(), - } - getCashTransactionsMessagesReq.Signature = signProtoMessage(t, getCashTransactionsMessagesReq, owner, false) - - getVerifiedExternalAppMessagesReq := &chatpb.GetMessagesRequest{ - ChatId: verifiedExternalAppChatId.ToProto(), - Owner: owner.ToProto(), - } - getVerifiedExternalAppMessagesReq.Signature = signProtoMessage(t, getVerifiedExternalAppMessagesReq, owner, false) - - getUnverifiedExternalAppMessagesReq := &chatpb.GetMessagesRequest{ - ChatId: unverifiedExternalAppChatId.ToProto(), - Owner: owner.ToProto(), - } - getUnverifiedExternalAppMessagesReq.Signature = signProtoMessage(t, getUnverifiedExternalAppMessagesReq, owner, false) - - getChatsResp, err := env.client.GetChats(env.ctx, getChatsReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetChatsResponse_NOT_FOUND, getChatsResp.Result) - assert.Empty(t, getChatsResp.Chats) - - getMessagesResp, err := env.client.GetMessages(env.ctx, getCodeTeamMessagesReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetMessagesResponse_NOT_FOUND, getMessagesResp.Result) - assert.Empty(t, getMessagesResp.Messages) - - expectedCodeTeamMessage := &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: "msg.body.key", - }, - }, - }, - { - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: chatpb.ExchangeDataContent_RECEIVED, - ExchangeData: &chatpb.ExchangeDataContent_Exact{ - Exact: &transactionpb.ExchangeData{ - Currency: "usd", - ExchangeRate: 0.1, - NativeAmount: 1.00, - Quarks: kin.ToQuarks(10), - }, - }, - }, - }, - }, - }, - } - env.sendInternalChatMessage(t, expectedCodeTeamMessage, chat_util.CodeTeamName, owner) - - expectedCashTransactionsMessage := &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: chatpb.ExchangeDataContent_GAVE, - ExchangeData: &chatpb.ExchangeDataContent_Exact{ - Exact: &transactionpb.ExchangeData{ - Currency: "usd", - ExchangeRate: 0.1, - NativeAmount: 4.2, - Quarks: kin.ToQuarks(42), - }, - }, - }, - }, - }, - }, - } - env.sendInternalChatMessage(t, expectedCashTransactionsMessage, chat_util.CashTransactionsName, owner) - - expectedVerifiedExternalAppMessage := &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_NaclBox{ - NaclBox: &chatpb.NaclBoxEncryptedContent{ - PeerPublicKey: testutil.NewRandomAccount(t).ToProto(), - Nonce: make([]byte, 24), - EncryptedPayload: []byte("verified message secret"), - }, - }, - }, - }, - } - env.sendExternalAppChatMessage(t, expectedVerifiedExternalAppMessage, testExternalAppDomain, true, owner) - - // Technically this type of message would never be allowed for an unverified chat - expectedUnverifiedExternalAppMessage := &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_NaclBox{ - NaclBox: &chatpb.NaclBoxEncryptedContent{ - PeerPublicKey: testutil.NewRandomAccount(t).ToProto(), - Nonce: make([]byte, 24), - EncryptedPayload: []byte("unverified message secret"), - }, - }, - }, - }, - } - env.sendExternalAppChatMessage(t, expectedUnverifiedExternalAppMessage, testExternalAppDomain, false, owner) - - getChatsResp, err = env.client.GetChats(env.ctx, getChatsReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetChatsResponse_OK, getChatsResp.Result) - require.Len(t, getChatsResp.Chats, 4) - - assert.Equal(t, codeTeamChatId[:], getChatsResp.Chats[0].ChatId.Value) - assert.Equal(t, "Code Team", getChatsResp.Chats[0].GetLocalized().KeyOrText) - assert.Nil(t, getChatsResp.Chats[0].ReadPointer) - assert.EqualValues(t, 1, getChatsResp.Chats[0].NumUnread) - assert.False(t, getChatsResp.Chats[0].IsMuted) - assert.True(t, getChatsResp.Chats[0].IsSubscribed) - assert.True(t, getChatsResp.Chats[0].CanMute) - assert.False(t, getChatsResp.Chats[0].CanUnsubscribe) - assert.True(t, getChatsResp.Chats[0].IsVerified) - - assert.Equal(t, cashTransactionsChatId[:], getChatsResp.Chats[1].ChatId.Value) - assert.Equal(t, localization.ChatTitleCashTransactions, getChatsResp.Chats[1].GetLocalized().KeyOrText) - assert.Nil(t, getChatsResp.Chats[1].ReadPointer) - assert.EqualValues(t, 0, getChatsResp.Chats[1].NumUnread) - assert.False(t, getChatsResp.Chats[1].IsMuted) - assert.True(t, getChatsResp.Chats[1].IsSubscribed) - assert.True(t, getChatsResp.Chats[1].CanMute) - assert.False(t, getChatsResp.Chats[1].CanUnsubscribe) - assert.True(t, getChatsResp.Chats[1].IsVerified) - - assert.Equal(t, verifiedExternalAppChatId[:], getChatsResp.Chats[2].ChatId.Value) - assert.Equal(t, testExternalAppDomain, getChatsResp.Chats[2].GetDomain().Value) - assert.Nil(t, getChatsResp.Chats[2].ReadPointer) - assert.EqualValues(t, 1, getChatsResp.Chats[2].NumUnread) - assert.False(t, getChatsResp.Chats[2].IsMuted) - assert.True(t, getChatsResp.Chats[2].IsSubscribed) - assert.True(t, getChatsResp.Chats[2].CanMute) - assert.True(t, getChatsResp.Chats[2].CanUnsubscribe) - assert.True(t, getChatsResp.Chats[2].IsVerified) - - assert.Equal(t, unverifiedExternalAppChatId[:], getChatsResp.Chats[3].ChatId.Value) - assert.Equal(t, testExternalAppDomain, getChatsResp.Chats[3].GetDomain().Value) - assert.Nil(t, getChatsResp.Chats[3].ReadPointer) - assert.EqualValues(t, 0, getChatsResp.Chats[3].NumUnread) - assert.False(t, getChatsResp.Chats[3].IsMuted) - assert.True(t, getChatsResp.Chats[3].IsSubscribed) - assert.True(t, getChatsResp.Chats[3].CanMute) - assert.True(t, getChatsResp.Chats[3].CanUnsubscribe) - assert.False(t, getChatsResp.Chats[3].IsVerified) - - getMessagesResp, err = env.client.GetMessages(env.ctx, getCodeTeamMessagesReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetMessagesResponse_OK, getMessagesResp.Result) - require.Len(t, getMessagesResp.Messages, 1) - assert.Equal(t, expectedCodeTeamMessage.MessageId.Value, getMessagesResp.Messages[0].Cursor.Value) - getMessagesResp.Messages[0].Cursor = nil - expectedCodeTeamMessage.Content[0].GetServerLocalized().KeyOrText = "localized message body content" - assert.True(t, proto.Equal(expectedCodeTeamMessage, getMessagesResp.Messages[0])) - - getMessagesResp, err = env.client.GetMessages(env.ctx, getCashTransactionsMessagesReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetMessagesResponse_OK, getMessagesResp.Result) - require.Len(t, getMessagesResp.Messages, 1) - assert.Equal(t, expectedCashTransactionsMessage.MessageId.Value, getMessagesResp.Messages[0].Cursor.Value) - getMessagesResp.Messages[0].Cursor = nil - assert.True(t, proto.Equal(expectedCashTransactionsMessage, getMessagesResp.Messages[0])) - - getMessagesResp, err = env.client.GetMessages(env.ctx, getVerifiedExternalAppMessagesReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetMessagesResponse_OK, getMessagesResp.Result) - require.Len(t, getMessagesResp.Messages, 1) - assert.Equal(t, expectedVerifiedExternalAppMessage.MessageId.Value, getMessagesResp.Messages[0].Cursor.Value) - getMessagesResp.Messages[0].Cursor = nil - assert.True(t, proto.Equal(expectedVerifiedExternalAppMessage, getMessagesResp.Messages[0])) - - getMessagesResp, err = env.client.GetMessages(env.ctx, getUnverifiedExternalAppMessagesReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetMessagesResponse_OK, getMessagesResp.Result) - require.Len(t, getMessagesResp.Messages, 1) - assert.Equal(t, expectedUnverifiedExternalAppMessage.MessageId.Value, getMessagesResp.Messages[0].Cursor.Value) - getMessagesResp.Messages[0].Cursor = nil - assert.True(t, proto.Equal(expectedUnverifiedExternalAppMessage, getMessagesResp.Messages[0])) -} - -func TestChatHistoryReadState_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - chatId := chat.GetChatId(chat_util.CodeTeamName, owner.PublicKey().ToBase58(), true) - - var messageIds []*chatpb.ChatMessageId - for i := 0; i < 5; i++ { - message := &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: fmt.Sprintf("msg.body.key%d", i), - }, - }, - }, - }, - } - messageIds = append(messageIds, message.MessageId) - env.sendInternalChatMessage(t, message, chat_util.CodeTeamName, owner) - } - - advancePointerReq := &chatpb.AdvancePointerRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - Pointer: &chatpb.Pointer{ - Kind: chatpb.Pointer_READ, - Value: messageIds[len(messageIds)/2], - }, - } - advancePointerReq.Signature = signProtoMessage(t, advancePointerReq, owner, false) - - advancePointerResp, err := env.client.AdvancePointer(env.ctx, advancePointerReq) - require.NoError(t, err) - assert.Equal(t, chatpb.AdvancePointerResponse_OK, advancePointerResp.Result) - - getChatsReq := &chatpb.GetChatsRequest{ - Owner: owner.ToProto(), - } - getChatsReq.Signature = signProtoMessage(t, getChatsReq, owner, false) - - getChatsResp, err := env.client.GetChats(env.ctx, getChatsReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetChatsResponse_OK, getChatsResp.Result) - require.Len(t, getChatsResp.Chats, 1) - assert.Equal(t, chatId[:], getChatsResp.Chats[0].ChatId.Value) - require.NotNil(t, getChatsResp.Chats[0].ReadPointer) - assert.Equal(t, messageIds[len(messageIds)/2].Value, getChatsResp.Chats[0].ReadPointer.Value.Value) -} - -func TestChatHistoryReadState_NegativeProgress(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - chatId := chat.GetChatId(chat_util.CodeTeamName, owner.PublicKey().ToBase58(), true) - - var messageIds []*chatpb.ChatMessageId - for i := 0; i < 5; i++ { - message := &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: fmt.Sprintf("msg.body.key%d", i), - }, - }, - }, - }, - } - messageIds = append(messageIds, message.MessageId) - env.sendInternalChatMessage(t, message, chat_util.CodeTeamName, owner) - } - - for i := len(messageIds) - 1; i >= 0; i-- { - advancePointerReq := &chatpb.AdvancePointerRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - Pointer: &chatpb.Pointer{ - Kind: chatpb.Pointer_READ, - Value: messageIds[i], - }, - } - advancePointerReq.Signature = signProtoMessage(t, advancePointerReq, owner, false) - - advancePointerResp, err := env.client.AdvancePointer(env.ctx, advancePointerReq) - require.NoError(t, err) - assert.Equal(t, chatpb.AdvancePointerResponse_OK, advancePointerResp.Result) - } - - getChatsReq := &chatpb.GetChatsRequest{ - Owner: owner.ToProto(), - } - getChatsReq.Signature = signProtoMessage(t, getChatsReq, owner, false) - - getChatsResp, err := env.client.GetChats(env.ctx, getChatsReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetChatsResponse_OK, getChatsResp.Result) - require.Len(t, getChatsResp.Chats, 1) - assert.Equal(t, chatId[:], getChatsResp.Chats[0].ChatId.Value) - require.NotNil(t, getChatsResp.Chats[0].ReadPointer) - assert.Equal(t, messageIds[len(messageIds)-1].Value, getChatsResp.Chats[0].ReadPointer.Value.Value) -} - -func TestChatHistoryReadState_ChatNotFound(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - chatId := chat.GetChatId(chat_util.CodeTeamName, owner.PublicKey().ToBase58(), true) - - advancePointerReq := &chatpb.AdvancePointerRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - Pointer: &chatpb.Pointer{ - Kind: chatpb.Pointer_READ, - Value: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - }, - } - advancePointerReq.Signature = signProtoMessage(t, advancePointerReq, owner, false) - - advancePointerResp, err := env.client.AdvancePointer(env.ctx, advancePointerReq) - require.NoError(t, err) - assert.Equal(t, chatpb.AdvancePointerResponse_CHAT_NOT_FOUND, advancePointerResp.Result) -} - -func TestChatHistoryReadState_MessageNotFound(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - - chatId := chat.GetChatId(chat_util.CodeTeamName, owner.PublicKey().ToBase58(), true) - - env.sendInternalChatMessage(t, &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: "msg.body.key", - }, - }, - }, - }, - }, chat_util.CodeTeamName, owner) - - advancePointerReq := &chatpb.AdvancePointerRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - Pointer: &chatpb.Pointer{ - Kind: chatpb.Pointer_READ, - Value: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - }, - } - advancePointerReq.Signature = signProtoMessage(t, advancePointerReq, owner, false) - - advancePointerResp, err := env.client.AdvancePointer(env.ctx, advancePointerReq) - require.NoError(t, err) - assert.Equal(t, chatpb.AdvancePointerResponse_MESSAGE_NOT_FOUND, advancePointerResp.Result) -} - -func TestChatMuteState_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - testExternalAppDomain := "test.com" - - chatId := chat.GetChatId(testExternalAppDomain, owner.PublicKey().ToBase58(), true) - - env.sendExternalAppChatMessage(t, &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_NaclBox{ - NaclBox: &chatpb.NaclBoxEncryptedContent{ - PeerPublicKey: testutil.NewRandomAccount(t).ToProto(), - Nonce: make([]byte, 24), - EncryptedPayload: []byte("secret"), - }, - }, - }, - }, - }, testExternalAppDomain, true, owner) - - for _, isMuted := range []bool{true, true, false, false, true, false, true} { - setMuteStateReq := &chatpb.SetMuteStateRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - IsMuted: isMuted, - } - setMuteStateReq.Signature = signProtoMessage(t, setMuteStateReq, owner, false) - - setSubscripionStatusResp, err := env.client.SetMuteState(env.ctx, setMuteStateReq) - require.NoError(t, err) - assert.Equal(t, chatpb.SetMuteStateResponse_OK, setSubscripionStatusResp.Result) - - getChatsReq := &chatpb.GetChatsRequest{ - Owner: owner.ToProto(), - } - getChatsReq.Signature = signProtoMessage(t, getChatsReq, owner, false) - - getChatsResp, err := env.client.GetChats(env.ctx, getChatsReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetChatsResponse_OK, getChatsResp.Result) - require.Len(t, getChatsResp.Chats, 1) - assert.Equal(t, chatId[:], getChatsResp.Chats[0].ChatId.Value) - assert.Equal(t, isMuted, getChatsResp.Chats[0].IsMuted) - } -} - -func TestChatMuteState_ChatNotFound(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - chatId := chat.GetChatId("test.com", owner.PublicKey().ToBase58(), false) - - setMuteStateReq := &chatpb.SetMuteStateRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - IsMuted: false, - } - setMuteStateReq.Signature = signProtoMessage(t, setMuteStateReq, owner, false) - - setMuteStateResp, err := env.client.SetMuteState(env.ctx, setMuteStateReq) - require.NoError(t, err) - assert.Equal(t, chatpb.SetMuteStateResponse_CHAT_NOT_FOUND, setMuteStateResp.Result) -} - -func TestChatMuteState_CantMute(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - chatId := chat.GetChatId(chat_util.TestCantMuteName, owner.PublicKey().ToBase58(), true) - - env.sendInternalChatMessage(t, &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: chatpb.ExchangeDataContent_GAVE, - ExchangeData: &chatpb.ExchangeDataContent_Exact{ - Exact: &transactionpb.ExchangeData{ - Currency: "usd", - ExchangeRate: 0.1, - NativeAmount: 4.2, - Quarks: kin.ToQuarks(42), - }, - }, - }, - }, - }, - }, - }, chat_util.TestCantMuteName, owner) - - setMuteStateReq := &chatpb.SetMuteStateRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - IsMuted: true, - } - setMuteStateReq.Signature = signProtoMessage(t, setMuteStateReq, owner, false) - - setMuteStateResp, err := env.client.SetMuteState(env.ctx, setMuteStateReq) - require.NoError(t, err) - assert.Equal(t, chatpb.SetMuteStateResponse_CANT_MUTE, setMuteStateResp.Result) - - getChatsReq := &chatpb.GetChatsRequest{ - Owner: owner.ToProto(), - } - getChatsReq.Signature = signProtoMessage(t, getChatsReq, owner, false) - - getChatsResp, err := env.client.GetChats(env.ctx, getChatsReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetChatsResponse_OK, getChatsResp.Result) - require.Len(t, getChatsResp.Chats, 1) - assert.Equal(t, chatId[:], getChatsResp.Chats[0].ChatId.Value) - assert.False(t, getChatsResp.Chats[0].IsMuted) -} - -func TestChatSubscriptionState_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - testExternalAppDomain := "test.com" - - chatId := chat.GetChatId(testExternalAppDomain, owner.PublicKey().ToBase58(), true) - - env.sendExternalAppChatMessage(t, &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_NaclBox{ - NaclBox: &chatpb.NaclBoxEncryptedContent{ - PeerPublicKey: testutil.NewRandomAccount(t).ToProto(), - Nonce: make([]byte, 24), - EncryptedPayload: []byte("secret"), - }, - }, - }, - }, - }, testExternalAppDomain, true, owner) - - for _, isSubscribed := range []bool{false, false, true, true, false, true, false} { - setSubscriptionStateReq := &chatpb.SetSubscriptionStateRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - IsSubscribed: isSubscribed, - } - setSubscriptionStateReq.Signature = signProtoMessage(t, setSubscriptionStateReq, owner, false) - - setSubscripionStatusResp, err := env.client.SetSubscriptionState(env.ctx, setSubscriptionStateReq) - require.NoError(t, err) - assert.Equal(t, chatpb.SetSubscriptionStateResponse_OK, setSubscripionStatusResp.Result) - - getChatsReq := &chatpb.GetChatsRequest{ - Owner: owner.ToProto(), - } - getChatsReq.Signature = signProtoMessage(t, getChatsReq, owner, false) - - getChatsResp, err := env.client.GetChats(env.ctx, getChatsReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetChatsResponse_OK, getChatsResp.Result) - require.Len(t, getChatsResp.Chats, 1) - assert.Equal(t, chatId[:], getChatsResp.Chats[0].ChatId.Value) - assert.Equal(t, isSubscribed, getChatsResp.Chats[0].IsSubscribed) - - if isSubscribed { - assert.EqualValues(t, 1, getChatsResp.Chats[0].NumUnread) - } else { - assert.EqualValues(t, 0, getChatsResp.Chats[0].NumUnread) - } - } -} - -func TestChatSubscriptionState_ChatNotFound(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - chatId := chat.GetChatId("test.com", owner.PublicKey().ToBase58(), false) - - setSubscriptionStateReq := &chatpb.SetSubscriptionStateRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - IsSubscribed: false, - } - setSubscriptionStateReq.Signature = signProtoMessage(t, setSubscriptionStateReq, owner, false) - - setSubscripionStatusResp, err := env.client.SetSubscriptionState(env.ctx, setSubscriptionStateReq) - require.NoError(t, err) - assert.Equal(t, chatpb.SetSubscriptionStateResponse_CHAT_NOT_FOUND, setSubscripionStatusResp.Result) -} - -func TestChatSubscriptionState_CantUnsubscribe(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - env.setupUserWithLocale(t, owner, language.English) - - chatId := chat.GetChatId(chat_util.TestCantUnsubscribeName, owner.PublicKey().ToBase58(), true) - - env.sendInternalChatMessage(t, &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_ExchangeData{ - ExchangeData: &chatpb.ExchangeDataContent{ - Verb: chatpb.ExchangeDataContent_GAVE, - ExchangeData: &chatpb.ExchangeDataContent_Exact{ - Exact: &transactionpb.ExchangeData{ - Currency: "usd", - ExchangeRate: 0.1, - NativeAmount: 4.2, - Quarks: kin.ToQuarks(42), - }, - }, - }, - }, - }, - }, - }, chat_util.TestCantUnsubscribeName, owner) - - setSubscriptionStateReq := &chatpb.SetSubscriptionStateRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - IsSubscribed: false, - } - setSubscriptionStateReq.Signature = signProtoMessage(t, setSubscriptionStateReq, owner, false) - - setSubscripionStatusResp, err := env.client.SetSubscriptionState(env.ctx, setSubscriptionStateReq) - require.NoError(t, err) - assert.Equal(t, chatpb.SetSubscriptionStateResponse_CANT_UNSUBSCRIBE, setSubscripionStatusResp.Result) - - getChatsReq := &chatpb.GetChatsRequest{ - Owner: owner.ToProto(), - } - getChatsReq.Signature = signProtoMessage(t, getChatsReq, owner, false) - - getChatsResp, err := env.client.GetChats(env.ctx, getChatsReq) - require.NoError(t, err) - assert.Equal(t, chatpb.GetChatsResponse_OK, getChatsResp.Result) - require.Len(t, getChatsResp.Chats, 1) - assert.Equal(t, chatId[:], getChatsResp.Chats[0].ChatId.Value) - assert.True(t, getChatsResp.Chats[0].IsSubscribed) -} - -func TestUnauthorizedAccess(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - maliciousUser := testutil.NewRandomAccount(t) - - chatId := chat.GetChatId(chat_util.CodeTeamName, owner.PublicKey().ToBase58(), true) - - env.sendInternalChatMessage(t, &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - Ts: timestamppb.Now(), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_ServerLocalized{ - ServerLocalized: &chatpb.ServerLocalizedContent{ - KeyOrText: "msg.body.key", - }, - }, - }, - }, - }, chat_util.CodeTeamName, owner) - - // - // GetChats - // - - getChatsReq := &chatpb.GetChatsRequest{ - Owner: owner.ToProto(), - } - getChatsReq.Signature = signProtoMessage(t, getChatsReq, owner, true) - - _, err := env.client.GetChats(env.ctx, getChatsReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - // - // GetMessages - // - - getMessagesReq := &chatpb.GetMessagesRequest{ - ChatId: chatId.ToProto(), - Owner: owner.ToProto(), - } - getMessagesReq.Signature = signProtoMessage(t, getMessagesReq, owner, true) - - _, err = env.client.GetMessages(env.ctx, getMessagesReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - getMessagesReq = &chatpb.GetMessagesRequest{ - ChatId: chatId.ToProto(), - Owner: maliciousUser.ToProto(), - } - getMessagesReq.Signature = signProtoMessage(t, getMessagesReq, maliciousUser, false) - - _, err = env.client.GetMessages(env.ctx, getMessagesReq) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) - - // - // AdvancePointer - // - - advancePointerReq := &chatpb.AdvancePointerRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - Pointer: &chatpb.Pointer{ - Kind: chatpb.Pointer_READ, - Value: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - }, - } - advancePointerReq.Signature = signProtoMessage(t, advancePointerReq, maliciousUser, false) - _, err = env.client.AdvancePointer(env.ctx, advancePointerReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - advancePointerReq = &chatpb.AdvancePointerRequest{ - Owner: maliciousUser.ToProto(), - ChatId: chatId.ToProto(), - Pointer: &chatpb.Pointer{ - Kind: chatpb.Pointer_READ, - Value: &chatpb.ChatMessageId{ - Value: testutil.NewRandomAccount(t).ToProto().Value, - }, - }, - } - advancePointerReq.Signature = signProtoMessage(t, advancePointerReq, maliciousUser, false) - _, err = env.client.AdvancePointer(env.ctx, advancePointerReq) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) - - // - // SetMuteState - // - - setMuteStateReq := &chatpb.SetMuteStateRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - IsMuted: true, - } - setMuteStateReq.Signature = signProtoMessage(t, setMuteStateReq, maliciousUser, false) - _, err = env.client.SetMuteState(env.ctx, setMuteStateReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - setMuteStateReq = &chatpb.SetMuteStateRequest{ - Owner: maliciousUser.ToProto(), - ChatId: chatId.ToProto(), - IsMuted: true, - } - setMuteStateReq.Signature = signProtoMessage(t, setMuteStateReq, maliciousUser, false) - _, err = env.client.SetMuteState(env.ctx, setMuteStateReq) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) - - // - // SetSubscriptionState - // - - setSubscriptionStateReq := &chatpb.SetSubscriptionStateRequest{ - Owner: owner.ToProto(), - ChatId: chatId.ToProto(), - IsSubscribed: false, - } - setSubscriptionStateReq.Signature = signProtoMessage(t, setSubscriptionStateReq, maliciousUser, false) - _, err = env.client.SetSubscriptionState(env.ctx, setSubscriptionStateReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - setSubscriptionStateReq = &chatpb.SetSubscriptionStateRequest{ - Owner: maliciousUser.ToProto(), - ChatId: chatId.ToProto(), - IsSubscribed: false, - } - setSubscriptionStateReq.Signature = signProtoMessage(t, setSubscriptionStateReq, maliciousUser, false) - _, err = env.client.SetSubscriptionState(env.ctx, setSubscriptionStateReq) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) -} - -type testEnv struct { - ctx context.Context - client chatpb.ChatClient - server *server - data code_data.Provider -} - -func setup(t *testing.T) (env *testEnv, cleanup func()) { - conn, serv, err := testutil.NewServer() - require.NoError(t, err) - - env = &testEnv{ - ctx: context.Background(), - client: chatpb.NewChatClient(conn), - data: code_data.NewTestDataProvider(), - } - - s := NewChatServer(env.data, auth_util.NewRPCSignatureVerifier(env.data)) - env.server = s.(*server) - - serv.RegisterService(func(server *grpc.Server) { - chatpb.RegisterChatServer(server, s) - }) - - cleanup, err = serv.Serve() - require.NoError(t, err) - return env, cleanup -} - -func (e *testEnv) sendExternalAppChatMessage(t *testing.T, msg *chatpb.ChatMessage, domain string, isVerified bool, recipient *common.Account) { - _, err := chat_util.SendChatMessage(e.ctx, e.data, domain, chat.ChatTypeExternalApp, isVerified, recipient, msg, false) - require.NoError(t, err) -} - -func (e *testEnv) sendInternalChatMessage(t *testing.T, msg *chatpb.ChatMessage, chatTitle string, recipient *common.Account) { - _, err := chat_util.SendChatMessage(e.ctx, e.data, chatTitle, chat.ChatTypeInternal, true, recipient, msg, false) - require.NoError(t, err) -} - -func (e *testEnv) setupUserWithLocale(t *testing.T, owner *common.Account, locale language.Tag) { - phoneNumber := "+12223334444" - containerId := user.NewDataContainerID() - - phoneVerificationRecord := &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: owner.PublicKey().ToBase58(), - LastVerifiedAt: time.Now(), - CreatedAt: time.Now(), - } - require.NoError(t, e.data.SavePhoneVerification(e.ctx, phoneVerificationRecord)) - - containerRecord := &storage.Record{ - ID: containerId, - OwnerAccount: owner.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, e.data.PutUserDataContainer(e.ctx, containerRecord)) - - userPreferencesRecord := preferences.GetDefaultPreferences(containerId) - userPreferencesRecord.Locale = locale - require.NoError(t, e.data.SaveUserPreferences(e.ctx, userPreferencesRecord)) -} - -func signProtoMessage(t *testing.T, msg proto.Message, signer *common.Account, simulateInvalidSignature bool) *commonpb.Signature { - msgBytes, err := proto.Marshal(msg) - require.NoError(t, err) - - if simulateInvalidSignature { - signer = testutil.NewRandomAccount(t) - } - - signature, err := signer.Sign(msgBytes) - require.NoError(t, err) - - return &commonpb.Signature{ - Value: signature, - } -} diff --git a/pkg/code/server/grpc/contact/server.go b/pkg/code/server/grpc/contact/server.go deleted file mode 100644 index 61c68a29..00000000 --- a/pkg/code/server/grpc/contact/server.go +++ /dev/null @@ -1,250 +0,0 @@ -package contact - -import ( - "context" - "time" - - "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - contactpb "github.com/code-payments/code-protobuf-api/generated/go/contact/v1" - - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/grpc/client" -) - -const ( - getContactsMaxPageSize = 1024 -) - -type contactListServer struct { - log *logrus.Entry - data code_data.Provider - auth *auth_util.RPCSignatureVerifier - - contactpb.UnimplementedContactListServer -} - -func NewContactListServer( - data code_data.Provider, - auth *auth_util.RPCSignatureVerifier, -) contactpb.ContactListServer { - return &contactListServer{ - log: logrus.StandardLogger().WithField("type", "contact/server"), - data: data, - auth: auth, - } -} - -func (s *contactListServer) AddContacts(ctx context.Context, req *contactpb.AddContactsRequest) (*contactpb.AddContactsResponse, error) { - log := s.log.WithField("method", "AddContacts") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - containerID, err := user.GetDataContainerIDFromProto(req.ContainerId) - if err != nil { - log.WithError(err).Warn("failure parsing data container id as uuid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("data_container", containerID.String()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.AuthorizeDataAccess(ctx, containerID, ownerAccount, req, signature); err != nil { - return nil, err - } - - contacts := make([]string, len(req.Contacts)) - for i, contact := range req.Contacts { - contacts[i] = contact.Value - } - - err = s.data.BatchAddContacts(ctx, containerID, contacts) - if err != nil { - log.WithError(err).Warn("failure adding contacts") - return nil, status.Error(codes.Internal, "") - } - - contactStatusByNumber, err := s.batchGetContactStatus(ctx, contacts) - if err != nil { - return nil, status.Error(codes.Internal, "") - } - - return &contactpb.AddContactsResponse{ - Result: contactpb.AddContactsResponse_OK, - ContactStatus: contactStatusByNumber, - }, nil -} - -func (s *contactListServer) RemoveContacts(ctx context.Context, req *contactpb.RemoveContactsRequest) (*contactpb.RemoveContactsResponse, error) { - log := s.log.WithField("method", "RemoveContacts") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - containerID, err := user.GetDataContainerIDFromProto(req.ContainerId) - if err != nil { - log.WithError(err).Warn("failure parsing data container id as uuid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("data_container", containerID.String()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.AuthorizeDataAccess(ctx, containerID, ownerAccount, req, signature); err != nil { - return nil, err - } - - contacts := make([]string, len(req.Contacts)) - for i, contact := range req.Contacts { - contacts[i] = contact.Value - } - - err = s.data.BatchRemoveContacts(ctx, containerID, contacts) - if err != nil { - log.WithError(err).Warn("failure removing contacts") - return nil, status.Error(codes.Internal, "") - } - - return &contactpb.RemoveContactsResponse{ - Result: contactpb.RemoveContactsResponse_OK, - }, nil -} - -func (s *contactListServer) GetContacts(ctx context.Context, req *contactpb.GetContactsRequest) (*contactpb.GetContactsResponse, error) { - log := s.log.WithField("method", "GetContacts") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - containerID, err := user.GetDataContainerIDFromProto(req.ContainerId) - if err != nil { - log.WithError(err).Warn("failure parsing data container id as uuid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("data_container", containerID.String()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.AuthorizeDataAccess(ctx, containerID, ownerAccount, req, signature); err != nil { - return nil, err - } - - var pageTokenBytes []byte - if req.PageToken != nil { - pageTokenBytes = req.PageToken.Value - } - - var contacts []*contactpb.Contact - var nextPageToken *contactpb.PageToken - - // We attempt to make as much meaningful progress in the contact list - // when IncludeOnlyInAppContacts is true to limit unneccessary network - // calls by clients with large address books. We also try to avoid - // taking too long by checkpointing after a certain amount of time, - // which eliminates the risk that these clients will endlessly timeout. - start := time.Now() - for { - limit := getContactsMaxPageSize - len(contacts) - 1 - - page, nextPageTokenBytes, err := s.data.GetContacts(ctx, containerID, uint32(limit), pageTokenBytes) - if err != nil { - log.WithError(err).Warn("failure fetching page of contacts") - return nil, status.Error(codes.Internal, "") - } - - contactStatusByNumber, err := s.batchGetContactStatus(ctx, page) - if err != nil { - return nil, status.Error(codes.Internal, "") - } - - for phoneNumber, contactStatus := range contactStatusByNumber { - if req.IncludeOnlyInAppContacts && !contactStatus.IsRegistered { - continue - } - - contacts = append(contacts, &contactpb.Contact{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Status: contactStatus, - }) - } - - if len(nextPageTokenBytes) > 0 { - pageTokenBytes = nextPageTokenBytes - nextPageToken = &contactpb.PageToken{ - Value: nextPageTokenBytes, - } - } else { - // Stop processing when we've reached the end of the contact list. - nextPageToken = nil - break - } - - if len(contacts) > getContactsMaxPageSize/2 { - // Stop processing when we've packed sufficient numbers into the - // page. We prefer to checkpoint than to slowly make progress on - // the contact list. - break - } - - if time.Since(start) > 500*time.Millisecond { - // Stop processing after a sufficient amount of time has passed. - // This eliminates potential timeouts at the client and allows it - // to checkpoint some progress via the page token. - break - } - } - - return &contactpb.GetContactsResponse{ - Result: contactpb.GetContactsResponse_OK, - NextPageToken: nextPageToken, - Contacts: contacts, - }, nil -} - -func (s *contactListServer) batchGetContactStatus(ctx context.Context, phoneNumbers []string) (map[string]*contactpb.ContactStatus, error) { - log := s.log.WithField("method", "batchGetContactStatus") - - result := make(map[string]*contactpb.ContactStatus) - for _, phoneNumber := range phoneNumbers { - result[phoneNumber] = &contactpb.ContactStatus{ - IsRegistered: false, - IsInvited: true, - } - } - - registered, err := s.data.FilterVerifiedPhoneNumbers(ctx, phoneNumbers) - if err != nil { - log.WithError(err).Warn("failure filtering registered users") - return nil, err - } - - for _, phoneNumber := range registered { - result[phoneNumber].IsRegistered = true - } - - return result, nil -} diff --git a/pkg/code/server/grpc/contact/server_test.go b/pkg/code/server/grpc/contact/server_test.go deleted file mode 100644 index acaf5357..00000000 --- a/pkg/code/server/grpc/contact/server_test.go +++ /dev/null @@ -1,499 +0,0 @@ -package contact - -import ( - "context" - "crypto/ed25519" - "fmt" - "testing" - "time" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/protobuf/proto" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - contactpb "github.com/code-payments/code-protobuf-api/generated/go/contact/v1" - - "github.com/code-payments/code-server/pkg/code/auth" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/storage" - "github.com/code-payments/code-server/pkg/testutil" -) - -type testEnv struct { - ctx context.Context - client contactpb.ContactListClient - server *contactListServer - data code_data.Provider -} - -func setup(t *testing.T) (env testEnv, cleanup func()) { - conn, serv, err := testutil.NewServer() - require.NoError(t, err) - - env.ctx = context.Background() - env.client = contactpb.NewContactListClient(conn) - env.data = code_data.NewTestDataProvider() - - s := NewContactListServer(env.data, auth.NewRPCSignatureVerifier(env.data)) - env.server = s.(*contactListServer) - - serv.RegisterService(func(server *grpc.Server) { - contactpb.RegisterContactListServer(server, s) - }) - - cleanup, err = serv.Serve() - require.NoError(t, err) - return env, cleanup -} - -func TestHappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount.PublicKey().ToBase58()) - - contactPhoneNumber := "+12223334444" - - addReq := &contactpb.AddContactsRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - Contacts: []*commonpb.PhoneNumber{ - { - Value: contactPhoneNumber, - }, - }, - } - - removeReq := &contactpb.RemoveContactsRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - Contacts: []*commonpb.PhoneNumber{ - { - Value: contactPhoneNumber, - }, - }, - } - - getReq := &contactpb.GetContactsRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - } - - reqBytes, err := proto.Marshal(addReq) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - addReq.Signature = &commonpb.Signature{ - Value: signature, - } - - reqBytes, err = proto.Marshal(removeReq) - require.NoError(t, err) - signature, err = ownerAccount.Sign(reqBytes) - require.NoError(t, err) - removeReq.Signature = &commonpb.Signature{ - Value: signature, - } - - reqBytes, err = proto.Marshal(getReq) - require.NoError(t, err) - signature, err = ownerAccount.Sign(reqBytes) - require.NoError(t, err) - getReq.Signature = &commonpb.Signature{ - Value: signature, - } - - getResp, err := env.client.GetContacts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, contactpb.GetContactsResponse_OK, getResp.Result) - assert.Nil(t, getResp.Contacts) - assert.Nil(t, getResp.NextPageToken) - - addResp, err := env.client.AddContacts(env.ctx, addReq) - require.NoError(t, err) - assert.Equal(t, contactpb.AddContactsResponse_OK, addResp.Result) - require.Len(t, addResp.ContactStatus, 1) - - _, ok := addResp.ContactStatus[contactPhoneNumber] - assert.True(t, ok) - - getResp, err = env.client.GetContacts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, contactpb.GetContactsResponse_OK, getResp.Result) - require.Len(t, getResp.Contacts, 1) - assert.Nil(t, getResp.NextPageToken) - assert.Equal(t, contactPhoneNumber, getResp.Contacts[0].PhoneNumber.Value) - - removeResp, err := env.client.RemoveContacts(env.ctx, removeReq) - require.NoError(t, err) - assert.Equal(t, contactpb.RemoveContactsResponse_OK, removeResp.Result) - - getResp, err = env.client.GetContacts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, contactpb.GetContactsResponse_OK, getResp.Result) - assert.Nil(t, getResp.Contacts) - assert.Nil(t, getResp.NextPageToken) -} - -func TestGetContacts_Paging(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount.PublicKey().ToBase58()) - - var phoneNumbers []string - for i := 0; i < 4; i++ { - var contacts []*commonpb.PhoneNumber - for j := 0; j < getContactsMaxPageSize; j++ { - phoneNumber := fmt.Sprintf("+1%d00%d", i, j) - phoneNumbers = append(phoneNumbers, phoneNumber) - contacts = append(contacts, &commonpb.PhoneNumber{ - Value: phoneNumber, - }) - } - - addReq := &contactpb.AddContactsRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - Contacts: contacts, - } - - reqBytes, err := proto.Marshal(addReq) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - addReq.Signature = &commonpb.Signature{ - Value: signature, - } - - addResp, err := env.client.AddContacts(env.ctx, addReq) - require.NoError(t, err) - assert.Equal(t, contactpb.AddContactsResponse_OK, addResp.Result) - } - - actualContacts := make(map[string]struct{}) - - var nextPageToken *contactpb.PageToken - var totalCalls int - for { - totalCalls++ - - getReq := &contactpb.GetContactsRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - PageToken: nextPageToken, - } - - reqBytes, err := proto.Marshal(getReq) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - getReq.Signature = &commonpb.Signature{ - Value: signature, - } - - getResp, err := env.client.GetContacts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, contactpb.GetContactsResponse_OK, getResp.Result) - - for _, actual := range getResp.Contacts { - actualContacts[actual.PhoneNumber.Value] = struct{}{} - } - - if getResp.NextPageToken == nil { - break - } - - nextPageToken = getResp.NextPageToken - } - - assert.True(t, totalCalls > 1) - - for _, phoneNumber := range phoneNumbers { - _, ok := actualContacts[phoneNumber] - assert.True(t, ok) - } -} - -func TestContactStatus(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount.PublicKey().ToBase58()) - - expectedContactStatus := map[string]*contactpb.ContactStatus{ - "+18005550001": { - IsInvited: true, - }, - "+18005550002": { - IsInvited: true, - IsRegistered: true, - }, - } - - var contacts []*commonpb.PhoneNumber - for phoneNumber, expectedStatus := range expectedContactStatus { - contacts = append(contacts, &commonpb.PhoneNumber{ - Value: phoneNumber, - }) - - if expectedStatus.IsRegistered { - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - } - } - - addReq := &contactpb.AddContactsRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - Contacts: contacts, - } - - getReq := &contactpb.GetContactsRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - } - - reqBytes, err := proto.Marshal(addReq) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - addReq.Signature = &commonpb.Signature{ - Value: signature, - } - - reqBytes, err = proto.Marshal(getReq) - require.NoError(t, err) - signature, err = ownerAccount.Sign(reqBytes) - require.NoError(t, err) - getReq.Signature = &commonpb.Signature{ - Value: signature, - } - - addResp, err := env.client.AddContacts(env.ctx, addReq) - require.NoError(t, err) - assert.Equal(t, contactpb.AddContactsResponse_OK, addResp.Result) - - getResp, err := env.client.GetContacts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, contactpb.GetContactsResponse_OK, getResp.Result) - - for phoneNumber, expectedStatus := range expectedContactStatus { - statusFromAdd, ok := addResp.ContactStatus[phoneNumber] - require.True(t, ok) - - var statusFromGet *contactpb.ContactStatus - for _, fetchedContact := range getResp.Contacts { - if fetchedContact.PhoneNumber.Value == phoneNumber { - statusFromGet = fetchedContact.Status - break - } - } - require.NotNil(t, statusFromGet) - - for _, actualStatus := range []*contactpb.ContactStatus{statusFromAdd, statusFromGet} { - assert.EqualValues(t, expectedStatus, actualStatus) - } - } - - getReq = &contactpb.GetContactsRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - IncludeOnlyInAppContacts: true, - } - - reqBytes, err = proto.Marshal(getReq) - require.NoError(t, err) - signature, err = ownerAccount.Sign(reqBytes) - require.NoError(t, err) - getReq.Signature = &commonpb.Signature{ - Value: signature, - } - - getResp, err = env.client.GetContacts(env.ctx, getReq) - require.NoError(t, err) - assert.Len(t, getResp.Contacts, 1) - for _, fetchedContact := range getResp.Contacts { - expectedStatus, ok := expectedContactStatus[fetchedContact.PhoneNumber.Value] - require.True(t, ok) - assert.True(t, expectedStatus.IsRegistered) - } -} - -func TestUnauthenticatedRPC(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerPrivateKey := testutil.GenerateSolanaKeypair(t) - ownerPublicKey := ownerPrivateKey.Public().(ed25519.PublicKey) - badPrivateKey := testutil.GenerateSolanaKeypair(t) - - containerID := uuid.New() - - contactPhoneNumber := "+12223334444" - - addReq := &contactpb.AddContactsRequest{ - OwnerAccountId: &commonpb.SolanaAccountId{ - Value: ownerPublicKey, - }, - ContainerId: &commonpb.DataContainerId{ - Value: containerID[:], - }, - Contacts: []*commonpb.PhoneNumber{ - { - Value: contactPhoneNumber, - }, - }, - } - - removeReq := &contactpb.RemoveContactsRequest{ - OwnerAccountId: &commonpb.SolanaAccountId{ - Value: ownerPublicKey, - }, - ContainerId: &commonpb.DataContainerId{ - Value: containerID[:], - }, - Contacts: []*commonpb.PhoneNumber{ - { - Value: contactPhoneNumber, - }, - }, - } - - getReq := &contactpb.GetContactsRequest{ - OwnerAccountId: &commonpb.SolanaAccountId{ - Value: ownerPublicKey, - }, - ContainerId: &commonpb.DataContainerId{ - Value: containerID[:], - }, - } - - reqBytes, err := proto.Marshal(addReq) - require.NoError(t, err) - addReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(badPrivateKey, reqBytes), - } - - reqBytes, err = proto.Marshal(removeReq) - require.NoError(t, err) - removeReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(badPrivateKey, reqBytes), - } - - reqBytes, err = proto.Marshal(getReq) - require.NoError(t, err) - getReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(badPrivateKey, reqBytes), - } - - _, err = env.client.GetContacts(env.ctx, getReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - _, err = env.client.AddContacts(env.ctx, addReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - _, err = env.client.RemoveContacts(env.ctx, removeReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) -} - -func TestUnauthorizedDataAccess(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - maliciousAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount.PublicKey().ToBase58()) - - contactPhoneNumber := "+12223334444" - - addReq := &contactpb.AddContactsRequest{ - OwnerAccountId: maliciousAccount.ToProto(), - ContainerId: containerID.Proto(), - Contacts: []*commonpb.PhoneNumber{ - { - Value: contactPhoneNumber, - }, - }, - } - - removeReq := &contactpb.RemoveContactsRequest{ - OwnerAccountId: maliciousAccount.ToProto(), - ContainerId: containerID.Proto(), - Contacts: []*commonpb.PhoneNumber{ - { - Value: contactPhoneNumber, - }, - }, - } - - getReq := &contactpb.GetContactsRequest{ - OwnerAccountId: maliciousAccount.ToProto(), - ContainerId: containerID.Proto(), - } - - reqBytes, err := proto.Marshal(addReq) - require.NoError(t, err) - signature, err := maliciousAccount.Sign(reqBytes) - require.NoError(t, err) - addReq.Signature = &commonpb.Signature{ - Value: signature, - } - - reqBytes, err = proto.Marshal(removeReq) - require.NoError(t, err) - signature, err = maliciousAccount.Sign(reqBytes) - require.NoError(t, err) - removeReq.Signature = &commonpb.Signature{ - Value: signature, - } - - reqBytes, err = proto.Marshal(getReq) - require.NoError(t, err) - signature, err = maliciousAccount.Sign(reqBytes) - require.NoError(t, err) - getReq.Signature = &commonpb.Signature{ - Value: signature, - } - - _, err = env.client.GetContacts(env.ctx, getReq) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) - - _, err = env.client.AddContacts(env.ctx, addReq) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) - - _, err = env.client.RemoveContacts(env.ctx, removeReq) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) -} - -func generateNewDataContainer(t *testing.T, env testEnv, ownerAccount string) *user.DataContainerID { - phoneNumber := "+12223334444" - - container := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount, - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUserDataContainer(env.ctx, container)) - return container.ID -} diff --git a/pkg/code/server/grpc/device/server.go b/pkg/code/server/grpc/device/server.go deleted file mode 100644 index f2b0f581..00000000 --- a/pkg/code/server/grpc/device/server.go +++ /dev/null @@ -1,134 +0,0 @@ -package device - -import ( - "context" - - "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - devicepb "github.com/code-payments/code-protobuf-api/generated/go/device/v1" - - "github.com/code-payments/code-server/pkg/grpc/client" - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/login" - "github.com/code-payments/code-server/pkg/code/data/phone" -) - -type server struct { - log *logrus.Entry - data code_data.Provider - auth *auth_util.RPCSignatureVerifier - - devicepb.UnimplementedDeviceServer -} - -func NewDeviceServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier) devicepb.DeviceServer { - return &server{ - log: logrus.StandardLogger().WithField("type", "device/server"), - data: data, - auth: auth, - } -} - -func (s *server) RegisterLoggedInAccounts(ctx context.Context, req *devicepb.RegisterLoggedInAccountsRequest) (*devicepb.RegisterLoggedInAccountsResponse, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "RegisterLoggedInAccounts", - "app_install": req.AppInstall.Value, - }) - log = client.InjectLoggingMetadata(ctx, log) - - if len(req.Owners) != len(req.Signatures) { - return nil, status.Error(codes.InvalidArgument, "") - } - - signatures := req.Signatures - req.Signatures = nil - - var validOwners []string - var invalidOwners []*commonpb.SolanaAccountId - - for i, protoOwner := range req.Owners { - owner, err := common.NewAccountFromProto(protoOwner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - - if err := s.auth.Authenticate(ctx, owner, req, signatures[i]); err != nil { - return nil, err - } - - _, err = s.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - invalidOwners = append(invalidOwners, protoOwner) - } else if err != nil { - log.WithError(err).Warn("failure checking phone verification status") - return nil, status.Error(codes.Internal, "") - } - - validOwners = append(validOwners, owner.PublicKey().ToBase58()) - } - - if len(invalidOwners) > 0 { - return &devicepb.RegisterLoggedInAccountsResponse{ - Result: devicepb.RegisterLoggedInAccountsResponse_INVALID_OWNER, - InvalidOwners: invalidOwners, - }, nil - } - - loginRecord := &login.MultiRecord{ - AppInstallId: req.AppInstall.Value, - Owners: validOwners, - } - err := s.data.SaveLogins(ctx, loginRecord) - if err != nil { - log.WithError(err).Warn("failure updating login records") - return nil, status.Error(codes.Internal, "") - } - - return &devicepb.RegisterLoggedInAccountsResponse{ - Result: devicepb.RegisterLoggedInAccountsResponse_OK, - }, nil -} - -func (s *server) GetLoggedInAccounts(ctx context.Context, req *devicepb.GetLoggedInAccountsRequest) (*devicepb.GetLoggedInAccountsResponse, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "GetLoggedInAccounts", - "app_install": req.AppInstall.Value, - }) - log = client.InjectLoggingMetadata(ctx, log) - - var protoOwners []*commonpb.SolanaAccountId - - loginRecord, err := s.data.GetLoginsByAppInstall(ctx, req.AppInstall.Value) - switch err { - case nil: - for _, owner := range loginRecord.Owners { - parsed, err := common.NewAccountFromPublicKeyString(owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - - protoOwners = append(protoOwners, parsed.ToProto()) - } - case login.ErrLoginNotFound: - default: - log.WithError(err).Warn("failure getting login records") - return nil, status.Error(codes.Internal, "") - } - - if len(protoOwners) > 1 { - log.Warn("unexpectedly have more than 1 owner logged into same app install") - return nil, status.Error(codes.Internal, "") - } - - return &devicepb.GetLoggedInAccountsResponse{ - Result: devicepb.GetLoggedInAccountsResponse_OK, - Owners: protoOwners, - }, nil -} diff --git a/pkg/code/server/grpc/device/server_test.go b/pkg/code/server/grpc/device/server_test.go deleted file mode 100644 index a54deba5..00000000 --- a/pkg/code/server/grpc/device/server_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package device - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/protobuf/proto" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - devicepb "github.com/code-payments/code-protobuf-api/generated/go/device/v1" - - "github.com/code-payments/code-server/pkg/testutil" - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/phone" -) - -func TestHappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - appInstallId := "app-install-id" - owner := testutil.NewRandomAccount(t) - - env.setupUser(t, owner) - - getReq := &devicepb.GetLoggedInAccountsRequest{ - AppInstall: &commonpb.AppInstallId{ - Value: appInstallId, - }, - } - getResp, err := env.client.GetLoggedInAccounts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, devicepb.GetLoggedInAccountsResponse_OK, getResp.Result) - assert.Empty(t, getResp.Owners) - - registerReq := &devicepb.RegisterLoggedInAccountsRequest{ - AppInstall: &commonpb.AppInstallId{ - Value: appInstallId, - }, - Owners: []*commonpb.SolanaAccountId{ - owner.ToProto(), - }, - } - registerReq.Signatures = []*commonpb.Signature{ - signProtoMessage(t, registerReq, owner, false), - } - - registerResp, err := env.client.RegisterLoggedInAccounts(env.ctx, registerReq) - require.NoError(t, err) - assert.Equal(t, devicepb.RegisterLoggedInAccountsResponse_OK, registerResp.Result) - assert.Empty(t, registerResp.InvalidOwners) - - getResp, err = env.client.GetLoggedInAccounts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, devicepb.GetLoggedInAccountsResponse_OK, getResp.Result) - require.Len(t, getResp.Owners, 1) - assert.Equal(t, owner.PublicKey().ToBytes(), getResp.Owners[0].Value) - - registerReq = &devicepb.RegisterLoggedInAccountsRequest{ - AppInstall: &commonpb.AppInstallId{ - Value: appInstallId, - }, - } - registerResp, err = env.client.RegisterLoggedInAccounts(env.ctx, registerReq) - require.NoError(t, err) - assert.Equal(t, devicepb.RegisterLoggedInAccountsResponse_OK, registerResp.Result) - assert.Empty(t, registerResp.InvalidOwners) - - getResp, err = env.client.GetLoggedInAccounts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, devicepb.GetLoggedInAccountsResponse_OK, getResp.Result) - assert.Empty(t, getResp.Owners) -} - -func TestInvalidOwner(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - appInstallId := "app-install-id" - owner := testutil.NewRandomAccount(t) - - registerReq := &devicepb.RegisterLoggedInAccountsRequest{ - AppInstall: &commonpb.AppInstallId{ - Value: appInstallId, - }, - Owners: []*commonpb.SolanaAccountId{ - owner.ToProto(), - }, - } - registerReq.Signatures = []*commonpb.Signature{ - signProtoMessage(t, registerReq, owner, false), - } - - registerResp, err := env.client.RegisterLoggedInAccounts(env.ctx, registerReq) - require.NoError(t, err) - assert.Equal(t, devicepb.RegisterLoggedInAccountsResponse_INVALID_OWNER, registerResp.Result) - require.Len(t, registerResp.InvalidOwners, 1) - assert.Equal(t, owner.PublicKey().ToBytes(), registerResp.InvalidOwners[0].Value) - - getReq := &devicepb.GetLoggedInAccountsRequest{ - AppInstall: &commonpb.AppInstallId{ - Value: appInstallId, - }, - } - getResp, err := env.client.GetLoggedInAccounts(env.ctx, getReq) - require.NoError(t, err) - assert.Equal(t, devicepb.GetLoggedInAccountsResponse_OK, getResp.Result) - assert.Empty(t, getResp.Owners) -} - -func TestUnauthorizedAccess(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - - registerReq := &devicepb.RegisterLoggedInAccountsRequest{ - AppInstall: &commonpb.AppInstallId{ - Value: "app-install-id", - }, - Owners: []*commonpb.SolanaAccountId{ - owner.ToProto(), - }, - } - registerReq.Signatures = []*commonpb.Signature{ - signProtoMessage(t, registerReq, owner, true), - } - - _, err := env.client.RegisterLoggedInAccounts(env.ctx, registerReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) -} - -type testEnv struct { - ctx context.Context - client devicepb.DeviceClient - server *server - data code_data.Provider -} - -func setup(t *testing.T) (env *testEnv, cleanup func()) { - conn, serv, err := testutil.NewServer() - require.NoError(t, err) - - env = &testEnv{ - ctx: context.Background(), - client: devicepb.NewDeviceClient(conn), - data: code_data.NewTestDataProvider(), - } - - s := NewDeviceServer(env.data, auth_util.NewRPCSignatureVerifier(env.data)) - env.server = s.(*server) - - serv.RegisterService(func(server *grpc.Server) { - devicepb.RegisterDeviceServer(server, s) - }) - - cleanup, err = serv.Serve() - require.NoError(t, err) - return env, cleanup -} - -func (e *testEnv) setupUser(t *testing.T, owner *common.Account) { - require.NoError(t, e.data.SavePhoneVerification(e.ctx, &phone.Verification{ - PhoneNumber: "+12223334444", - OwnerAccount: owner.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) -} - -func signProtoMessage(t *testing.T, msg proto.Message, signer *common.Account, simulateInvalidSignature bool) *commonpb.Signature { - msgBytes, err := proto.Marshal(msg) - require.NoError(t, err) - - if simulateInvalidSignature { - signer = testutil.NewRandomAccount(t) - } - - signature, err := signer.Sign(msgBytes) - require.NoError(t, err) - - return &commonpb.Signature{ - Value: signature, - } -} diff --git a/pkg/code/server/grpc/invite/v2/server.go b/pkg/code/server/grpc/invite/v2/server.go deleted file mode 100644 index 82086d59..00000000 --- a/pkg/code/server/grpc/invite/v2/server.go +++ /dev/null @@ -1,54 +0,0 @@ -package invite - -import ( - "context" - - "github.com/sirupsen/logrus" - - invitepb "github.com/code-payments/code-protobuf-api/generated/go/invite/v2" - - "github.com/code-payments/code-server/pkg/phone" - - code_data "github.com/code-payments/code-server/pkg/code/data" -) - -// The invite system has been removed, so any calls to this RPC results in success -// for legacy clients. -type inviteServer struct { - log *logrus.Entry - data code_data.Provider - phoneVerifier phone.Verifier - - invitepb.UnimplementedInviteServer -} - -func NewInviteServer( - data code_data.Provider, - phoneVerifier phone.Verifier, -) invitepb.InviteServer { - return &inviteServer{ - log: logrus.StandardLogger().WithField("type", "invite/v2/server"), - data: data, - phoneVerifier: phoneVerifier, - } -} - -func (s *inviteServer) GetInviteCount(ctx context.Context, req *invitepb.GetInviteCountRequest) (*invitepb.GetInviteCountResponse, error) { - return &invitepb.GetInviteCountResponse{ - Result: invitepb.GetInviteCountResponse_OK, - InviteCount: 0, - }, nil -} - -func (s *inviteServer) InvitePhoneNumber(ctx context.Context, req *invitepb.InvitePhoneNumberRequest) (*invitepb.InvitePhoneNumberResponse, error) { - return &invitepb.InvitePhoneNumberResponse{ - Result: invitepb.InvitePhoneNumberResponse_OK, - }, nil -} - -func (s *inviteServer) GetInvitationStatus(ctx context.Context, req *invitepb.GetInvitationStatusRequest) (*invitepb.GetInvitationStatusResponse, error) { - return &invitepb.GetInvitationStatusResponse{ - Result: invitepb.GetInvitationStatusResponse_OK, - Status: invitepb.InvitationStatus_INVITED, - }, nil -} diff --git a/pkg/code/server/grpc/micropayment/server.go b/pkg/code/server/grpc/micropayment/server.go deleted file mode 100644 index 4ccb59fc..00000000 --- a/pkg/code/server/grpc/micropayment/server.go +++ /dev/null @@ -1,317 +0,0 @@ -package micropayment - -import ( - "context" - "encoding/base64" - "encoding/binary" - "math/rand" - "strings" - "time" - - "github.com/mr-tron/base58" - "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" - micropaymentpb "github.com/code-payments/code-protobuf-api/generated/go/micropayment/v1" - - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/paymentrequest" - "github.com/code-payments/code-server/pkg/code/data/paywall" - "github.com/code-payments/code-server/pkg/code/data/webhook" - "github.com/code-payments/code-server/pkg/code/limit" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/netutil" - "github.com/code-payments/code-server/pkg/retry" -) - -const ( - codifiedContentUrlBase = "getcode.com/m/" -) - -type microPaymentServer struct { - log *logrus.Entry - - data code_data.Provider - - auth *auth_util.RPCSignatureVerifier - - micropaymentpb.UnimplementedMicroPaymentServer -} - -func NewMicroPaymentServer( - data code_data.Provider, - auth *auth_util.RPCSignatureVerifier, -) micropaymentpb.MicroPaymentServer { - return µPaymentServer{ - log: logrus.StandardLogger().WithField("type", "micropayment/v1/server"), - data: data, - auth: auth, - } -} - -func (s *microPaymentServer) GetStatus(ctx context.Context, req *micropaymentpb.GetStatusRequest) (*micropaymentpb.GetStatusResponse, error) { - log := s.log.WithField("method", "GetStatus") - log = client.InjectLoggingMetadata(ctx, log) - - intentId := base58.Encode(req.IntentId.Value) - log = log.WithField("intent", intentId) - - resp := µpaymentpb.GetStatusResponse{ - Exists: false, - CodeScanned: false, - IntentSubmitted: false, - } - - _, err := s.data.GetRequest(ctx, intentId) - if err == paymentrequest.ErrPaymentRequestNotFound { - return resp, nil - } else if err != nil { - log.WithError(err).Warn("failure getting request record") - return nil, status.Error(codes.Internal, "") - } - resp.Exists = true - - messageRecords, err := s.data.GetMessages(ctx, intentId) - if err != nil { - log.WithError(err).Warn("failure getting message records") - return nil, status.Error(codes.Internal, "") - } - - for _, messageRecord := range messageRecords { - var message messagingpb.Message - if err := proto.Unmarshal(messageRecord.Message, &message); err != nil { - log.WithError(err).Warn("failure unmarshalling message bytes") - continue - } - - switch message.Kind.(type) { - case *messagingpb.Message_CodeScanned: - resp.CodeScanned = true - } - - if resp.CodeScanned { - break - } - } - - intentRecord, err := s.data.GetIntent(ctx, intentId) - switch err { - case nil: - resp.IntentSubmitted = intentRecord.State != intent.StateRevoked - case intent.ErrIntentNotFound: - default: - log.WithError(err).Warn("failure getting intent record") - return nil, status.Error(codes.Internal, "") - } - - return resp, nil -} - -func (s *microPaymentServer) RegisterWebhook(ctx context.Context, req *micropaymentpb.RegisterWebhookRequest) (*micropaymentpb.RegisterWebhookResponse, error) { - log := s.log.WithField("method", "RegisterWebhook") - log = client.InjectLoggingMetadata(ctx, log) - - intentId := base58.Encode(req.IntentId.Value) - log = log.WithField("intent", intentId) - - err := netutil.ValidateHttpUrl(req.Url, false, false) - if err != nil { - log.WithField("url", req.Url).WithError(err).Info("url failed validation") - return µpaymentpb.RegisterWebhookResponse{ - Result: micropaymentpb.RegisterWebhookResponse_INVALID_URL, - }, nil - } - - // todo: distributed lock on intent id - - _, err = s.data.GetIntent(ctx, intentId) - if err == nil { - return µpaymentpb.RegisterWebhookResponse{ - Result: micropaymentpb.RegisterWebhookResponse_INTENT_EXISTS, - }, nil - } else if err != intent.ErrIntentNotFound { - log.WithError(err).Warn("failure checking intent status") - return nil, status.Error(codes.Internal, "") - } - - _, err = s.data.GetRequest(ctx, intentId) - if err == paymentrequest.ErrPaymentRequestNotFound { - return µpaymentpb.RegisterWebhookResponse{ - Result: micropaymentpb.RegisterWebhookResponse_REQUEST_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure checking request status") - return nil, status.Error(codes.Internal, "") - } - - record := &webhook.Record{ - WebhookId: intentId, - Url: req.Url, - Type: webhook.TypeIntentSubmitted, - - Attempts: 0, - State: webhook.StateUnknown, - - CreatedAt: time.Now(), - NextAttemptAt: nil, - } - err = s.data.CreateWebhook(ctx, record) - if err == webhook.ErrAlreadyExists { - return µpaymentpb.RegisterWebhookResponse{ - Result: micropaymentpb.RegisterWebhookResponse_ALREADY_REGISTERED, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure creating webhook record") - return nil, status.Error(codes.Internal, "") - } - - return µpaymentpb.RegisterWebhookResponse{ - Result: micropaymentpb.RegisterWebhookResponse_OK, - }, nil -} - -func (s *microPaymentServer) Codify(ctx context.Context, req *micropaymentpb.CodifyRequest) (*micropaymentpb.CodifyResponse, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "Codify", - }) - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.OwnerAccount) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - destination, err := common.NewAccountFromProto(req.PrimaryAccount) - if err != nil { - log.WithError(err).Warn("invalid destination account") - return nil, status.Error(codes.Internal, "") - } - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - err = netutil.ValidateHttpUrl(req.Url, false, true) - if err != nil { - log.WithField("url", req.Url).WithError(err).Info("url failed validation") - return µpaymentpb.CodifyResponse{ - Result: micropaymentpb.CodifyResponse_INVALID_URL, - }, nil - } - - primaryAccountInfoRecord, err := s.data.GetLatestAccountInfoByOwnerAddressAndType(ctx, owner.PublicKey().ToBase58(), commonpb.AccountType_PRIMARY) - if err == account.ErrAccountInfoNotFound { - return µpaymentpb.CodifyResponse{ - Result: micropaymentpb.CodifyResponse_INVALID_ACCOUNT, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting primary account info record") - return nil, status.Error(codes.Internal, "") - } - - if primaryAccountInfoRecord.TokenAccount != destination.PublicKey().ToBase58() { - return µpaymentpb.CodifyResponse{ - Result: micropaymentpb.CodifyResponse_INVALID_ACCOUNT, - }, nil - } - - limits, ok := limit.MicroPaymentLimits[currency_lib.Code(req.Currency)] - if !ok { - return µpaymentpb.CodifyResponse{ - Result: micropaymentpb.CodifyResponse_UNSUPPORTED_CURRENCY, - }, nil - } else if req.NativeAmount > limits.Max || req.NativeAmount < limits.Min { - return µpaymentpb.CodifyResponse{ - Result: micropaymentpb.CodifyResponse_NATIVE_AMOUNT_EXCEEDS_LIMIT, - }, nil - } - - var shortPath string - _, err = retry.Retry( // In the unlikely event getRandomShortPath has a collision - func() error { - shortPath = getRandomShortPath() - - return s.data.CreatePaywall(ctx, &paywall.Record{ - OwnerAccount: owner.PublicKey().ToBase58(), - DestinationTokenAccount: destination.PublicKey().ToBase58(), - - ExchangeCurrency: currency_lib.Code(req.Currency), - NativeAmount: req.NativeAmount, - RedirectUrl: req.Url, - ShortPath: shortPath, - - Signature: base58.Encode(signature.Value), - - CreatedAt: time.Now(), - }) - }, - retry.RetriableErrors(paywall.ErrPaywallExists), - retry.Limit(3), - ) - if err != nil { - log.WithError(err).Warn("failure creating paywall record") - return nil, status.Error(codes.Internal, "") - } - - return µpaymentpb.CodifyResponse{ - Result: micropaymentpb.CodifyResponse_OK, - CodifiedUrl: codifiedContentUrlBase + shortPath, - }, nil -} - -func (s *microPaymentServer) GetPathMetadata(ctx context.Context, req *micropaymentpb.GetPathMetadataRequest) (*micropaymentpb.GetPathMetadataResponse, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "GetPathMetadata", - "path": req.Path, - }) - log = client.InjectLoggingMetadata(ctx, log) - - paywallRecord, err := s.data.GetPaywallByShortPath(ctx, req.Path) - if err == paywall.ErrPaywallNotFound { - return µpaymentpb.GetPathMetadataResponse{ - Result: micropaymentpb.GetPathMetadataResponse_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting paywall record") - return nil, status.Error(codes.Internal, "") - } - - destination, err := common.NewAccountFromPublicKeyString(paywallRecord.DestinationTokenAccount) - if err != nil { - log.WithError(err).Warn("invalid destination account") - return nil, status.Error(codes.Internal, "") - } - - // Hard-coded stub implementation to enable a PoC - return µpaymentpb.GetPathMetadataResponse{ - Result: micropaymentpb.GetPathMetadataResponse_OK, - Destination: destination.ToProto(), - Currency: string(paywallRecord.ExchangeCurrency), - NativeAmount: paywallRecord.NativeAmount, - RedirctUrl: paywallRecord.RedirectUrl, - }, nil -} - -// Something stupidly simple to start -func getRandomShortPath() string { - rawValue := make([]byte, 4) - binary.LittleEndian.PutUint32(rawValue, rand.Uint32()) - path := base64.StdEncoding.EncodeToString(rawValue) - path = strings.Replace(path, "/", "", -1) - path = strings.Replace(path, "=", "", -1) - path = strings.Replace(path, "+", "", -1) - return path -} diff --git a/pkg/code/server/grpc/phone/server.go b/pkg/code/server/grpc/phone/server.go deleted file mode 100644 index a4acc823..00000000 --- a/pkg/code/server/grpc/phone/server.go +++ /dev/null @@ -1,443 +0,0 @@ -package phone - -import ( - "context" - "time" - - "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - phonepb "github.com/code-payments/code-protobuf-api/generated/go/phone/v1" - - "github.com/code-payments/code-server/pkg/code/antispam" - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/grpc/client" - phone_lib "github.com/code-payments/code-server/pkg/phone" -) - -const ( - // todo: all of this needs to be configurable - - maxSmsSendAttempts = 3 - maxCheckCodeAttempts = 3 - - clientSMSTimeout = 60 * time.Second - - maxTokenChecks = 5 - tokenExpiryDuration = 1 * time.Hour -) - -type phoneVerificationServer struct { - log *logrus.Entry - data code_data.Provider - auth *auth_util.RPCSignatureVerifier - guard *antispam.Guard - phoneVerifier phone_lib.Verifier - phonepb.UnimplementedPhoneVerificationServer - - // todo: Help simplify testing, but should be cleaned up. - disableClientOptimizations bool -} - -func NewPhoneVerificationServer( - data code_data.Provider, - auth *auth_util.RPCSignatureVerifier, - guard *antispam.Guard, - phoneVerifier phone_lib.Verifier, -) phonepb.PhoneVerificationServer { - return &phoneVerificationServer{ - log: logrus.StandardLogger().WithField("type", "phone/server"), - data: data, - auth: auth, - guard: guard, - phoneVerifier: phoneVerifier, - } -} - -func (s *phoneVerificationServer) SendVerificationCode(ctx context.Context, req *phonepb.SendVerificationCodeRequest) (*phonepb.SendVerificationCodeResponse, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "SendVerificationCode", - "phone": req.PhoneNumber.Value, - }) - log = client.InjectLoggingMetadata(ctx, log) - - var deviceToken *string - if req.DeviceToken != nil { - deviceToken = &req.DeviceToken.Value - } - - // todo: distributed lock on the phone number - - // The latest event for a sent verification will provide additional context with - // respect to the verification that we're potentially operating on. - // - // Note: There's no way to get a verification by phone number in Twilio, so - // we have to rely on previous events for this piece of data. It'll be - // largely correct, unless there was an issue with writing to the DB. - latestSmsSendEvent, err := s.data.GetLatestPhoneEventForNumberByType( - ctx, req.PhoneNumber.Value, phone.EventTypeVerificationCodeSent, - ) - if err != nil && err != phone.ErrEventNotFound { - log.WithError(err).Warn("failure getting latest sms sent event") - return nil, status.Error(codes.Internal, "") - } - - var isActiveVerification bool - if latestSmsSendEvent != nil { - isActiveVerification, err = s.phoneVerifier.IsVerificationActive(ctx, latestSmsSendEvent.VerificationId) - if err != nil { - log.WithError(err).Warn("failure checking if phone has an active verification") - return nil, status.Error(codes.Internal, "") - } - } - - if isActiveVerification { - currentVerification := latestSmsSendEvent.VerificationId - - exceedsSmsSendAttempts, err := s.exceedsCustomSmsSendAttempts(ctx, currentVerification) - if err != nil { - log.WithError(err).Warn("failure querying sms send attempts") - return nil, status.Error(codes.Internal, "") - } - - exceedsCheckAttempts, err := s.exceedsCustomCheckCodeAttempts(ctx, currentVerification) - if err != nil { - log.WithError(err).Warn("failure querying verification check attempts") - return nil, status.Error(codes.Internal, "") - } - - // Cancel any verifications that exceed our custom limits, which should be less - // than Twilio's. This will ensure that users don't get caught in overly aggressive - // rate limiting on Twilio's end. - if exceedsSmsSendAttempts || exceedsCheckAttempts { - err := s.phoneVerifier.Cancel(ctx, currentVerification) - if err != nil { - log.WithError(err).Warn("failure canceling current verification") - return nil, status.Error(codes.Internal, "") - } - isActiveVerification = false - } - } - - // Now that we're custom managing verifications on behalf of Twilio, it's on us - // to do antispam checks to avoid excessive new verifications and sent SMS messages. - if !isActiveVerification { - allow, reason, err := s.guard.AllowNewPhoneVerification(ctx, req.PhoneNumber.Value, deviceToken) - if err != nil { - log.WithError(err).Warn("failure performing antispam check") - return nil, status.Error(codes.Internal, "") - } else if !allow { - var resultCode phonepb.SendVerificationCodeResponse_Result - switch reason { - case antispam.ReasonUnsupportedCountry: - resultCode = phonepb.SendVerificationCodeResponse_UNSUPPORTED_COUNTRY - case antispam.ReasonUnsupportedDevice: - resultCode = phonepb.SendVerificationCodeResponse_UNSUPPORTED_DEVICE - default: - resultCode = phonepb.SendVerificationCodeResponse_RATE_LIMITED - } - return &phonepb.SendVerificationCodeResponse{ - Result: resultCode, - }, nil - } - } - - // Allow clients to go back and forth between phone and verification input - // screens by faking a successful response without going to Twilio. This is - // a nice workaround that avoids hitting rate limits and causing friction on - // the client. - if !s.disableClientOptimizations && isActiveVerification { - if time.Since(latestSmsSendEvent.CreatedAt) < clientSMSTimeout-5*time.Second { - return &phonepb.SendVerificationCodeResponse{ - Result: phonepb.SendVerificationCodeResponse_OK, - }, nil - } - } - - allow, err := s.guard.AllowSendSmsVerificationCode(ctx, req.PhoneNumber.Value) - if err != nil { - log.WithError(err).Warn("failure performing antispam check") - return nil, status.Error(codes.Internal, "") - } else if !allow { - return &phonepb.SendVerificationCodeResponse{ - Result: phonepb.SendVerificationCodeResponse_RATE_LIMITED, - }, nil - } - - var result phonepb.SendVerificationCodeResponse_Result - verificationId, phoneMetadata, err := s.phoneVerifier.SendCode(ctx, req.PhoneNumber.Value) - switch err { - case nil: - result = phonepb.SendVerificationCodeResponse_OK - - // Save an event indicating a SMS text with a verification code has been - // sent. This can mark the start of a new verification flow and will be - // used by subsequent events to pull things like the verification ID, as - // Twilio doesn't make it possible to get verifications by phone number. - event := &phone.Event{ - Type: phone.EventTypeVerificationCodeSent, - - VerificationId: verificationId, - - PhoneNumber: req.PhoneNumber.Value, - PhoneMetadata: phoneMetadata, - - CreatedAt: time.Now(), - } - err := s.data.PutPhoneEvent(ctx, event) - if err != nil { - // We're in a bit of a pickle. The SMS is sent, but our custom management - // could be broken. It's better to propagate an error and have the client - // retry. If the DB is down, we can't do anything interesting with the - // verification anyways. - log.WithError(err).Warn("failure saving event") - return nil, status.Error(codes.Internal, "") - } - - case phone_lib.ErrInvalidNumber: - result = phonepb.SendVerificationCodeResponse_INVALID_PHONE_NUMBER - case phone_lib.ErrUnsupportedPhoneType: - result = phonepb.SendVerificationCodeResponse_UNSUPPORTED_PHONE_TYPE - case phone_lib.ErrRateLimited: - // Ideally should never happen now that we're micro-managing Twilio - result = phonepb.SendVerificationCodeResponse_RATE_LIMITED - default: - log.WithError(err).Warn("failure sending verification code") - return nil, status.Error(codes.Internal, "") - } - - return &phonepb.SendVerificationCodeResponse{ - Result: result, - }, nil -} - -func (s *phoneVerificationServer) CheckVerificationCode(ctx context.Context, req *phonepb.CheckVerificationCodeRequest) (*phonepb.CheckVerificationCodeResponse, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "CheckVerificationCode", - "phone": req.PhoneNumber.Value, - "code": req.Code.Value, - }) - log = client.InjectLoggingMetadata(ctx, log) - - // todo: distributed lock on the phone number - - // The latest event for a sent verification will provide additional context with - // respect to the verification that we're operating on. - // - // Note: There's no way to get a verification by phone number in Twilio, so - // we have to rely on previous events for this piece of data. It'll be - // largely correct, unless there was an issue with writing to the DB. - latestSmsSendEvent, err := s.data.GetLatestPhoneEventForNumberByType( - ctx, req.PhoneNumber.Value, phone.EventTypeVerificationCodeSent, - ) - if err == phone.ErrEventNotFound { - return &phonepb.CheckVerificationCodeResponse{ - Result: phonepb.CheckVerificationCodeResponse_NO_VERIFICATION, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting latest sms sent event") - return nil, status.Error(codes.Internal, "") - } - - // Get the current amount of checks already attempted for this verification. - // If our threshold is exceeded, which must be less than to Twilio's, then - // we'll fake the verification no longer existing. The client will be able to - // restart the flow where we'll cancel the verification on their behalf. - maxAttemptsBreached, err := s.exceedsCustomCheckCodeAttempts(ctx, latestSmsSendEvent.VerificationId) - if err != nil { - log.WithError(err).Warn("failure querying verification check attempts") - return nil, status.Error(codes.Internal, "") - } else if maxAttemptsBreached { - return &phonepb.CheckVerificationCodeResponse{ - Result: phonepb.CheckVerificationCodeResponse_NO_VERIFICATION, - }, nil - } - - allow, err := s.guard.AllowCheckSmsVerificationCode(ctx, req.PhoneNumber.Value) - if err != nil { - log.WithError(err).Warn("failure performing antispam check") - return nil, status.Error(codes.Internal, "") - } else if !allow { - return &phonepb.CheckVerificationCodeResponse{ - Result: phonepb.CheckVerificationCodeResponse_RATE_LIMITED, - }, nil - } - - // Save an event indicating we've gone out to Twilio to check the verification - // code - checkCodeEvent := &phone.Event{ - Type: phone.EventTypeCheckVerificationCode, - - VerificationId: latestSmsSendEvent.VerificationId, - PhoneMetadata: latestSmsSendEvent.PhoneMetadata, - - PhoneNumber: req.PhoneNumber.Value, - - CreatedAt: time.Now(), - } - err = s.data.PutPhoneEvent(ctx, checkCodeEvent) - if err != nil { - log.WithError(err).Warn("failure saving event") - return nil, status.Error(codes.Internal, "") - } - - var result phonepb.CheckVerificationCodeResponse_Result - - err = s.phoneVerifier.Check(ctx, req.PhoneNumber.Value, req.Code.Value) - switch err { - case nil: - result = phonepb.CheckVerificationCodeResponse_OK - - // Save a one-time use linking token, which the client can later use to - // map their phone number to an owner account. - token := &phone.LinkingToken{ - PhoneNumber: req.PhoneNumber.Value, - Code: req.Code.Value, - CurrentCheckCount: 0, - MaxCheckCount: maxTokenChecks, - ExpiresAt: time.Now().Add(tokenExpiryDuration), - } - err := s.data.SavePhoneLinkingToken(ctx, token) - if err != nil { - log.WithError(err).Warn("failure saving token") - return nil, status.Error(codes.Internal, "") - } - - // Save an event indicating the phone verification process is completed. - // The event would only be used for analysis on large scale attacks like - // toll fraud, where completion rates fall significantly. - verificationCompletedEvent := &phone.Event{ - Type: phone.EventTypeVerificationCompleted, - - VerificationId: latestSmsSendEvent.VerificationId, - PhoneMetadata: latestSmsSendEvent.PhoneMetadata, - - PhoneNumber: req.PhoneNumber.Value, - - CreatedAt: time.Now(), - } - err = s.data.PutPhoneEvent(ctx, verificationCompletedEvent) - if err != nil { - // No need to fail the RPC call, as this doesn't affect any user flows. - log.WithError(err).Warn("failure saving event") - } - case phone_lib.ErrInvalidVerificationCode: - result = phonepb.CheckVerificationCodeResponse_INVALID_CODE - - maxAttemptsBreached, err := s.exceedsCustomCheckCodeAttempts(ctx, latestSmsSendEvent.VerificationId) - if err != nil { - // No need to explicitly fail the RPC, since either result is fine - // and an INTERNAL error is the worse than an INVALID_CODE result - // code. - log.WithError(err).Warn("failure querying verification check attempts") - } else if maxAttemptsBreached { - // The last attempt failed the code check. We prefer to return NO_VERIFICATION, - // so the user can restart the flow immediately without having to another code. - // It'll be perceived better than restarting the flow on a subsequent attempt - // with the correct code. - result = phonepb.CheckVerificationCodeResponse_NO_VERIFICATION - } - case phone_lib.ErrNoVerification: - result = phonepb.CheckVerificationCodeResponse_NO_VERIFICATION - default: - log.WithError(err).Warn("failure checking verification code") - return nil, status.Error(codes.Internal, "") - } - - if result != phonepb.CheckVerificationCodeResponse_OK { - return &phonepb.CheckVerificationCodeResponse{ - Result: result, - }, nil - } - - return &phonepb.CheckVerificationCodeResponse{ - Result: result, - LinkingToken: &phonepb.PhoneLinkingToken{ - PhoneNumber: req.PhoneNumber, - Code: req.Code, - }, - }, nil -} - -func (s *phoneVerificationServer) GetAssociatedPhoneNumber(ctx context.Context, req *phonepb.GetAssociatedPhoneNumberRequest) (*phonepb.GetAssociatedPhoneNumberResponse, error) { - log := s.log.WithField("method", "GetAssociatedPhoneNumber") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, ownerAccount, req, signature); err != nil { - return nil, err - } - - latestVerificationForAccount, err := s.data.GetLatestPhoneVerificationForAccount(ctx, ownerAccount.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - return &phonepb.GetAssociatedPhoneNumberResponse{ - Result: phonepb.GetAssociatedPhoneNumberResponse_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting latest verification record for owner account") - return nil, status.Error(codes.Internal, "") - } - - log = log.WithField("phone", latestVerificationForAccount.PhoneNumber) - - ownerManagementState, err := common.GetOwnerManagementState(ctx, s.data, ownerAccount) - if err != nil { - log.WithError(err).Warn("failure getting owner management state") - return nil, status.Error(codes.Internal, "") - } else if ownerManagementState == common.OwnerManagementStateUnlocked { - return &phonepb.GetAssociatedPhoneNumberResponse{ - Result: phonepb.GetAssociatedPhoneNumberResponse_UNLOCKED_TIMELOCK_ACCOUNT, - }, nil - } - - isLinked, err := s.data.IsPhoneNumberLinkedToAccount(ctx, latestVerificationForAccount.PhoneNumber, ownerAccount.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting link status for owner account") - return nil, status.Error(codes.Internal, "") - } - - return &phonepb.GetAssociatedPhoneNumberResponse{ - Result: phonepb.GetAssociatedPhoneNumberResponse_OK, - PhoneNumber: &commonpb.PhoneNumber{ - Value: latestVerificationForAccount.PhoneNumber, - }, - IsLinked: isLinked, - }, nil -} - -func (s *phoneVerificationServer) exceedsCustomSmsSendAttempts(ctx context.Context, verification string) (bool, error) { - smsSendAttempts, err := s.data.GetPhoneEventCountForVerificationByType( - ctx, verification, phone.EventTypeVerificationCodeSent, - ) - if err != nil { - return false, err - } - - // todo: configurable - return smsSendAttempts >= maxSmsSendAttempts, nil -} - -func (s *phoneVerificationServer) exceedsCustomCheckCodeAttempts(ctx context.Context, verification string) (bool, error) { - checkAttempts, err := s.data.GetPhoneEventCountForVerificationByType( - ctx, verification, phone.EventTypeCheckVerificationCode, - ) - if err != nil { - return false, err - } - - // todo: configurable - return checkAttempts >= maxCheckCodeAttempts, nil -} diff --git a/pkg/code/server/grpc/phone/server_test.go b/pkg/code/server/grpc/phone/server_test.go deleted file mode 100644 index 1be6ebad..00000000 --- a/pkg/code/server/grpc/phone/server_test.go +++ /dev/null @@ -1,639 +0,0 @@ -package phone - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/protobuf/proto" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - phonepb "github.com/code-payments/code-protobuf-api/generated/go/phone/v1" - - "github.com/code-payments/code-server/pkg/code/antispam" - "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/phone" - memory_device_verifier "github.com/code-payments/code-server/pkg/device/memory" - phone_lib "github.com/code-payments/code-server/pkg/phone" - memory_phone_client "github.com/code-payments/code-server/pkg/phone/memory" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" - "github.com/code-payments/code-server/pkg/testutil" -) - -type testEnv struct { - ctx context.Context - client phonepb.PhoneVerificationClient - server *phoneVerificationServer - data code_data.Provider - verifier phone_lib.Verifier -} - -func setup(t *testing.T) (env testEnv, cleanup func()) { - conn, serv, err := testutil.NewServer() - require.NoError(t, err) - - env.ctx = context.Background() - env.client = phonepb.NewPhoneVerificationClient(conn) - env.data = code_data.NewTestDataProvider() - env.verifier = memory_phone_client.NewVerifier() - - disabledAntispamGuard := antispam.NewGuard( - env.data, - memory_device_verifier.NewMemoryDeviceVerifier(), - nil, - antispam.WithPhoneVerificationsPerInterval(100), - antispam.WithTimePerSmsVerificationCodeSend(0), - antispam.WithTimePerSmsVerificationCheck(0), - ) - - testutil.SetupRandomSubsidizer(t, env.data) - - s := NewPhoneVerificationServer(env.data, auth.NewRPCSignatureVerifier(env.data), disabledAntispamGuard, env.verifier) - env.server = s.(*phoneVerificationServer) - serv.RegisterService(func(server *grpc.Server) { - phonepb.RegisterPhoneVerificationServer(server, s) - }) - - cleanup, err = serv.Serve() - require.NoError(t, err) - return env, cleanup -} - -func TestSMSVerification_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - phoneNumber := "+12223334444" - - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - smsSentEvent, err := env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCodeSent) - require.NoError(t, err) - - assert.Equal(t, phone.EventTypeVerificationCodeSent, smsSentEvent.Type) - assert.NotEmpty(t, smsSentEvent.VerificationId) - assert.Equal(t, phoneNumber, smsSentEvent.PhoneNumber) - - checkResp, err := env.client.CheckVerificationCode(env.ctx, &phonepb.CheckVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: memory_phone_client.ValidPhoneVerificationToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.CheckVerificationCodeResponse_OK, checkResp.Result) - assert.Equal(t, phoneNumber, checkResp.LinkingToken.PhoneNumber.Value) - assert.Equal(t, memory_phone_client.ValidPhoneVerificationToken, checkResp.LinkingToken.Code.Value) - - checkCodeEvent, err := env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeCheckVerificationCode) - require.NoError(t, err) - - assert.Equal(t, phone.EventTypeCheckVerificationCode, checkCodeEvent.Type) - assert.Equal(t, smsSentEvent.VerificationId, checkCodeEvent.VerificationId) - assert.Equal(t, phoneNumber, checkCodeEvent.PhoneNumber) - - verificationCompletedEvent, err := env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCompleted) - require.NoError(t, err) - - assert.Equal(t, phone.EventTypeVerificationCompleted, verificationCompletedEvent.Type) - assert.Equal(t, smsSentEvent.VerificationId, verificationCompletedEvent.VerificationId) - assert.Equal(t, phoneNumber, verificationCompletedEvent.PhoneNumber) - - err = env.data.UsePhoneLinkingToken(env.ctx, phoneNumber, memory_phone_client.ValidPhoneVerificationToken) - require.NoError(t, err) - - err = env.data.UsePhoneLinkingToken(env.ctx, phoneNumber, memory_phone_client.ValidPhoneVerificationToken) - assert.Equal(t, phone.ErrLinkingTokenNotFound, err) -} - -func TestSendVerificationCode_ExceedCustomSmsSendLimit(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - env.server.disableClientOptimizations = true - - phoneNumber := "+12223334444" - - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - smsSentEvent, err := env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCodeSent) - require.NoError(t, err) - - for i := 0; i < 5; i++ { - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - isActive, err := env.verifier.IsVerificationActive(env.ctx, smsSentEvent.VerificationId) - require.NoError(t, err) - assert.Equal(t, i < maxSmsSendAttempts-1, isActive) - } - - _, err = env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCompleted) - assert.Equal(t, phone.ErrEventNotFound, err) -} - -func TestSendVerificationCode_AntispamGuard_TimePerSmsVerificationCodeSend(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - env.server.guard = antispam.NewGuard( - env.data, - memory_device_verifier.NewMemoryDeviceVerifier(), - nil, - antispam.WithPhoneVerificationsPerInterval(10), - antispam.WithTimePerSmsVerificationCodeSend(5*time.Second), - ) - - phoneNumber := "+12223334444" - - sendCodeReq := &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - } - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, sendCodeReq) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - // Guard shouldn't be hit - sendCodeResp, err = env.client.SendVerificationCode(env.ctx, sendCodeReq) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - // We shouldn't have sent out another verification code - count, err := env.data.GetPhoneEventCountForNumberByTypeSinceTimestamp(env.ctx, phoneNumber, phone.EventTypeVerificationCodeSent, time.Now().Add(-1*time.Minute)) - require.NoError(t, err) - assert.EqualValues(t, 1, count) -} - -func TestSendVerificationCode_AntispamGuard_NewVerificationsLimit(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - env.server.guard = antispam.NewGuard( - env.data, - memory_device_verifier.NewMemoryDeviceVerifier(), - nil, - antispam.WithPhoneVerificationsPerInterval(1), - antispam.WithTimePerSmsVerificationCodeSend(0), - ) - - phoneNumber := "+12223334444" - - sendCodeReq := &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - } - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, sendCodeReq) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - checkResp, err := env.client.CheckVerificationCode(env.ctx, &phonepb.CheckVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: memory_phone_client.ValidPhoneVerificationToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.CheckVerificationCodeResponse_OK, checkResp.Result) - - sendCodeResp, err = env.client.SendVerificationCode(env.ctx, sendCodeReq) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_RATE_LIMITED, sendCodeResp.Result) -} - -func TestSendVerificationCode_AntispamGuard_DeviceVerification(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - env.server.guard = antispam.NewGuard( - env.data, - memory_device_verifier.NewMemoryDeviceVerifier(), - nil, - antispam.WithPhoneVerificationsPerInterval(1), - antispam.WithTimePerSmsVerificationCodeSend(0), - ) - - phoneNumber := "+12223334444" - - sendCodeReq := &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.InvalidDeviceToken, - }, - } - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, sendCodeReq) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_UNSUPPORTED_DEVICE, sendCodeResp.Result) -} - -func TestCheckVerificationCode_InvalidCode(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - phoneNumber := "+12223334444" - - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - checkResp, err := env.client.CheckVerificationCode(env.ctx, &phonepb.CheckVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: memory_phone_client.InvalidPhoneVerificationToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.CheckVerificationCodeResponse_INVALID_CODE, checkResp.Result) - assert.Nil(t, checkResp.LinkingToken) - - _, err = env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCompleted) - assert.Equal(t, phone.ErrEventNotFound, err) -} - -func TestCheckVerificationCode_NoActiveVerification(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - phoneNumber := "+12223334444" - - resp, err := env.client.CheckVerificationCode(env.ctx, &phonepb.CheckVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: memory_phone_client.ValidPhoneVerificationToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.CheckVerificationCodeResponse_NO_VERIFICATION, resp.Result) - assert.Nil(t, resp.LinkingToken) - - _, err = env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCompleted) - assert.Equal(t, phone.ErrEventNotFound, err) -} - -func TestCheckVerificationCode_VerificationAlreadyCompleted(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - phoneNumber := "+12223334444" - - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - err = env.verifier.Check(env.ctx, phoneNumber, memory_phone_client.ValidPhoneVerificationToken) - require.NoError(t, err) - - resp, err := env.client.CheckVerificationCode(env.ctx, &phonepb.CheckVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: memory_phone_client.ValidPhoneVerificationToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.CheckVerificationCodeResponse_NO_VERIFICATION, resp.Result) - assert.Nil(t, resp.LinkingToken) - - _, err = env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCompleted) - assert.Equal(t, phone.ErrEventNotFound, err) -} - -func TestCheckVerificationCode_ExceedCustomCodeCheckLimit(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - phoneNumber := "+12223334444" - - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - smsSentEvent, err := env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCodeSent) - require.NoError(t, err) - - for i := 0; i < 5; i++ { - resp, err := env.client.CheckVerificationCode(env.ctx, &phonepb.CheckVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: memory_phone_client.InvalidPhoneVerificationToken, - }, - }) - require.NoError(t, err) - - if i < maxCheckCodeAttempts-1 { - assert.Equal(t, phonepb.CheckVerificationCodeResponse_INVALID_CODE, resp.Result) - } else { - assert.Equal(t, phonepb.CheckVerificationCodeResponse_NO_VERIFICATION, resp.Result) - } - } - - isActive, err := env.verifier.IsVerificationActive(env.ctx, smsSentEvent.VerificationId) - require.NoError(t, err) - assert.True(t, isActive) - - sendCodeResp, err = env.client.SendVerificationCode(env.ctx, &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - isActive, err = env.verifier.IsVerificationActive(env.ctx, smsSentEvent.VerificationId) - require.NoError(t, err) - assert.False(t, isActive) - - _, err = env.data.GetLatestPhoneEventForNumberByType(env.ctx, phoneNumber, phone.EventTypeVerificationCompleted) - assert.Equal(t, phone.ErrEventNotFound, err) -} - -func TestCheckVerificationCode_AntispamGuard(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - env.server.guard = antispam.NewGuard( - env.data, - memory_device_verifier.NewMemoryDeviceVerifier(), - nil, - antispam.WithTimePerSmsVerificationCheck(5*time.Second), - ) - - phoneNumber := "+12223334444" - - sendCodeResp, err := env.client.SendVerificationCode(env.ctx, &phonepb.SendVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - DeviceToken: &commonpb.DeviceToken{ - Value: memory_device_verifier.ValidDeviceToken, - }, - }) - require.NoError(t, err) - assert.Equal(t, phonepb.SendVerificationCodeResponse_OK, sendCodeResp.Result) - - checkReq := &phonepb.CheckVerificationCodeRequest{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: memory_phone_client.InvalidPhoneVerificationToken, - }, - } - checkResp, err := env.client.CheckVerificationCode(env.ctx, checkReq) - require.NoError(t, err) - assert.Equal(t, phonepb.CheckVerificationCodeResponse_INVALID_CODE, checkResp.Result) - - checkResp, err = env.client.CheckVerificationCode(env.ctx, checkReq) - require.NoError(t, err) - assert.Equal(t, phonepb.CheckVerificationCodeResponse_RATE_LIMITED, checkResp.Result) -} - -func TestGetAssociatedPhoneNumber_NotFound(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - req := &phonepb.GetAssociatedPhoneNumberRequest{ - OwnerAccountId: ownerAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.GetAssociatedPhoneNumber(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, phonepb.GetAssociatedPhoneNumberResponse_NOT_FOUND, resp.Result) - assert.Nil(t, resp.PhoneNumber) - assert.False(t, resp.IsLinked) -} - -func TestGetAssociatedPhoneNumber_LinkStatus(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - phoneNumber := "+12223334444" - - req := &phonepb.GetAssociatedPhoneNumberRequest{ - OwnerAccountId: ownerAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - - resp, err := env.client.GetAssociatedPhoneNumber(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, phonepb.GetAssociatedPhoneNumberResponse_OK, resp.Result) - assert.Equal(t, phoneNumber, resp.PhoneNumber.Value) - assert.True(t, resp.IsLinked) - - newOwnerAccount := testutil.NewRandomAccount(t) - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: newOwnerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - - resp, err = env.client.GetAssociatedPhoneNumber(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, phonepb.GetAssociatedPhoneNumberResponse_OK, resp.Result) - assert.Equal(t, phoneNumber, resp.PhoneNumber.Value) - assert.False(t, resp.IsLinked) - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - - for _, isUnlinked := range []bool{false, true} { - require.NoError(t, env.data.SaveOwnerAccountPhoneSetting(env.ctx, phoneNumber, &phone.OwnerAccountSetting{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IsUnlinked: &isUnlinked, - CreatedAt: time.Now(), - LastUpdatedAt: time.Now(), - })) - - resp, err = env.client.GetAssociatedPhoneNumber(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, phonepb.GetAssociatedPhoneNumberResponse_OK, resp.Result) - assert.Equal(t, phoneNumber, resp.PhoneNumber.Value) - assert.Equal(t, !isUnlinked, resp.IsLinked) - } -} - -func TestGetAssociatedPhoneNumber_UnlockedTimelockAccount(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - phoneNumber := "+12223334444" - - req := &phonepb.GetAssociatedPhoneNumberRequest{ - OwnerAccountId: ownerAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - require.NoError(t, err) - timelockRecord := timelockAccounts.ToDBRecord() - require.NoError(t, env.data.SaveTimelock(env.ctx, timelockRecord)) - - accountInfoRecord := &account.Record{ - OwnerAccount: timelockRecord.VaultOwner, - AuthorityAccount: timelockRecord.VaultOwner, - TokenAccount: timelockRecord.VaultAddress, - MintAccount: timelockAccounts.Mint.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_PRIMARY, - } - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) - - resp, err := env.client.GetAssociatedPhoneNumber(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, phonepb.GetAssociatedPhoneNumberResponse_OK, resp.Result) - - timelockRecord.VaultState = timelock_token.StateUnlocked - timelockRecord.Block += 1 - require.NoError(t, env.data.SaveTimelock(env.ctx, timelockRecord)) - - resp, err = env.client.GetAssociatedPhoneNumber(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, phonepb.GetAssociatedPhoneNumberResponse_UNLOCKED_TIMELOCK_ACCOUNT, resp.Result) - assert.Nil(t, resp.PhoneNumber) -} - -func TestUnauthenticatedRPC(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - maliciousAccount := testutil.NewRandomAccount(t) - - req := &phonepb.GetAssociatedPhoneNumberRequest{ - OwnerAccountId: ownerAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := maliciousAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - _, err = env.client.GetAssociatedPhoneNumber(env.ctx, req) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) -} diff --git a/pkg/code/server/grpc/push/server.go b/pkg/code/server/grpc/push/server.go deleted file mode 100644 index 729ae99c..00000000 --- a/pkg/code/server/grpc/push/server.go +++ /dev/null @@ -1,124 +0,0 @@ -package push - -import ( - "context" - "time" - - "github.com/sirupsen/logrus" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - pushpb "github.com/code-payments/code-protobuf-api/generated/go/push/v1" - - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/push" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/grpc/client" - push_lib "github.com/code-payments/code-server/pkg/push" -) - -type pushServer struct { - log *logrus.Entry - data code_data.Provider - auth *auth_util.RPCSignatureVerifier - pushProvider push_lib.Provider - - pushpb.UnimplementedPushServer -} - -func NewPushServer( - data code_data.Provider, - auth *auth_util.RPCSignatureVerifier, - pushProvider push_lib.Provider, -) pushpb.PushServer { - return &pushServer{ - log: logrus.StandardLogger().WithField("type", "push/server"), - data: data, - auth: auth, - pushProvider: pushProvider, - } -} - -func (s *pushServer) AddToken(ctx context.Context, req *pushpb.AddTokenRequest) (*pushpb.AddTokenResponse, error) { - log := s.log.WithField("method", "AddToken") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - containerID, err := user.GetDataContainerIDFromProto(req.ContainerId) - if err != nil { - log.WithError(err).Warn("failure parsing data container id as uuid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("data_container", containerID.String()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.AuthorizeDataAccess(ctx, containerID, ownerAccount, req, signature); err != nil { - return nil, err - } - - userAgent, err := client.GetUserAgent(ctx) - if err != nil { - return nil, status.Error(codes.InvalidArgument, "invalid user-agent header value") - } - - var tokenType push.TokenType - - switch userAgent.DeviceType { - case client.DeviceTypeAndroid: - if req.TokenType != pushpb.TokenType_FCM_ANDROID { - return nil, status.Error(codes.InvalidArgument, "android client must specify an android token type") - } - - tokenType = push.TokenTypeFcmAndroid - case client.DeviceTypeIOS: - if req.TokenType != pushpb.TokenType_FCM_APNS { - return nil, status.Error(codes.InvalidArgument, "ios client must specify an apns token type") - } - - tokenType = push.TokenTypeFcmApns - default: - return nil, status.Error(codes.InvalidArgument, "unsupported user-agent device type") - } - - isValid, err := s.pushProvider.IsValidPushToken(ctx, req.PushToken) - if err != nil { - log.WithError(err).Warn("failure checking push token validity") - return nil, status.Error(codes.Internal, "") - } else if !isValid { - return &pushpb.AddTokenResponse{ - Result: pushpb.AddTokenResponse_INVALID_PUSH_TOKEN, - }, nil - } - - record := &push.Record{ - DataContainerId: *containerID, - - PushToken: req.PushToken, - TokenType: tokenType, - IsValid: true, - - CreatedAt: time.Now(), - } - if req.AppInstall != nil { - record.AppInstallId = &req.AppInstall.Value - } - - err = s.data.PutPushToken(ctx, record) - if err != nil && err != push.ErrTokenExists { - log.WithError(err).Warn("failure saving push token") - return nil, status.Error(codes.Internal, "") - } - - return &pushpb.AddTokenResponse{ - Result: pushpb.AddTokenResponse_OK, - }, nil -} diff --git a/pkg/code/server/grpc/push/server_test.go b/pkg/code/server/grpc/push/server_test.go deleted file mode 100644 index 2d94cfb8..00000000 --- a/pkg/code/server/grpc/push/server_test.go +++ /dev/null @@ -1,381 +0,0 @@ -package push - -import ( - "context" - "testing" - "time" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/protobuf/proto" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - pushpb "github.com/code-payments/code-protobuf-api/generated/go/push/v1" - - "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/push" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/storage" - memory_push "github.com/code-payments/code-server/pkg/push/memory" - "github.com/code-payments/code-server/pkg/testutil" -) - -func TestAddToken_HappyPath_AndroidToken(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - client := newAndroidClient(t, env) - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount) - - req := makeAddFcmAndroidTokenReq(t, ownerAccount, *containerID) - resp, err := client.AddToken(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, pushpb.AddTokenResponse_OK, resp.Result) - - records, err := env.data.GetAllValidPushTokensdByDataContainer(env.ctx, containerID) - require.NoError(t, err) - require.Len(t, records, 1) - - record := records[0] - assert.Equal(t, req.PushToken, record.PushToken) - assert.Equal(t, push.TokenTypeFcmAndroid, record.TokenType) - assert.True(t, record.IsValid) - require.NotNil(t, record.AppInstallId) - assert.Equal(t, req.AppInstall.Value, *record.AppInstallId) -} - -func TestAddToken_HappyPath_APNSToken(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - client := newIOSClient(t, env) - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount) - - req := makeAddFcmApnsTokenReq(t, ownerAccount, *containerID) - resp, err := client.AddToken(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, pushpb.AddTokenResponse_OK, resp.Result) - - records, err := env.data.GetAllValidPushTokensdByDataContainer(env.ctx, containerID) - require.NoError(t, err) - require.Len(t, records, 1) - - record := records[0] - assert.Equal(t, req.PushToken, record.PushToken) - assert.Equal(t, push.TokenTypeFcmApns, record.TokenType) - assert.True(t, record.IsValid) - require.NotNil(t, record.AppInstallId) - assert.Equal(t, req.AppInstall.Value, *record.AppInstallId) -} - -func TestAddToken_HappyPath_MultipleTokens(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - androidClient := newAndroidClient(t, env) - iosClient := newIOSClient(t, env) - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount) - - androidTokenReq := makeAddFcmAndroidTokenReq(t, ownerAccount, *containerID) - resp, err := androidClient.AddToken(env.ctx, androidTokenReq) - require.NoError(t, err) - assert.Equal(t, pushpb.AddTokenResponse_OK, resp.Result) - - apnsTokenReq := makeAddFcmApnsTokenReq(t, ownerAccount, *containerID) - resp, err = iosClient.AddToken(env.ctx, apnsTokenReq) - require.NoError(t, err) - assert.Equal(t, pushpb.AddTokenResponse_OK, resp.Result) - - records, err := env.data.GetAllValidPushTokensdByDataContainer(env.ctx, containerID) - require.NoError(t, err) - require.Len(t, records, 2) - - record := records[0] - assert.Equal(t, androidTokenReq.PushToken, record.PushToken) - assert.Equal(t, push.TokenTypeFcmAndroid, record.TokenType) - assert.True(t, record.IsValid) - require.NotNil(t, record.AppInstallId) - assert.Equal(t, androidTokenReq.AppInstall.Value, *record.AppInstallId) - - record = records[1] - assert.Equal(t, apnsTokenReq.PushToken, record.PushToken) - assert.Equal(t, push.TokenTypeFcmApns, record.TokenType) - assert.True(t, record.IsValid) - require.NotNil(t, record.AppInstallId) - assert.Equal(t, apnsTokenReq.AppInstall.Value, *record.AppInstallId) -} - -func TestAddToken_InvalidToken(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - client := newAndroidClient(t, env) - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount) - - invalidTokenReq := makeAddReqWithInvalidToken(t, ownerAccount, *containerID) - resp, err := client.AddToken(env.ctx, invalidTokenReq) - require.NoError(t, err) - assert.Equal(t, pushpb.AddTokenResponse_INVALID_PUSH_TOKEN, resp.Result) - - _, err = env.data.GetAllValidPushTokensdByDataContainer(env.ctx, containerID) - assert.Equal(t, push.ErrTokenNotFound, err) -} - -func TestAddToken_UserAgentValidation(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount) - - androidTokenReq := makeAddFcmAndroidTokenReq(t, ownerAccount, *containerID) - apnsTokenReq := makeAddFcmApnsTokenReq(t, ownerAccount, *containerID) - - // No user-agent header value - client := newClientWithoutUserAgent(t, env) - _, err := client.AddToken(env.ctx, androidTokenReq) - testutil.AssertStatusErrorWithCode(t, err, codes.InvalidArgument) - - // Android client setting an APNS push token - client = newAndroidClient(t, env) - _, err = client.AddToken(env.ctx, apnsTokenReq) - testutil.AssertStatusErrorWithCode(t, err, codes.InvalidArgument) - - // iOS client setting an Android push token - client = newIOSClient(t, env) - _, err = client.AddToken(env.ctx, androidTokenReq) - testutil.AssertStatusErrorWithCode(t, err, codes.InvalidArgument) - - // No push tokens should be saved - _, err = env.data.GetAllValidPushTokensdByDataContainer(env.ctx, containerID) - assert.Equal(t, push.ErrTokenNotFound, err) -} - -func TestAddToken_Idempotency(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - client := newAndroidClient(t, env) - - ownerAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount) - - invalidTokenReq := makeAddFcmAndroidTokenReq(t, ownerAccount, *containerID) - for i := 0; i < 5; i++ { - resp, err := client.AddToken(env.ctx, invalidTokenReq) - require.NoError(t, err) - assert.Equal(t, pushpb.AddTokenResponse_OK, resp.Result) - } - - records, err := env.data.GetAllValidPushTokensdByDataContainer(env.ctx, containerID) - require.NoError(t, err) - assert.Len(t, records, 1) -} - -func TestUnauthenticatedRPC(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - client := newAndroidClient(t, env) - - ownerAccount := testutil.NewRandomAccount(t) - - maliciousAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount) - - req := &pushpb.AddTokenRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - PushToken: memory_push.ValidAndroidPushToken, - TokenType: pushpb.TokenType_FCM_ANDROID, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := maliciousAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - _, err = client.AddToken(env.ctx, req) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) -} - -func TestUnauthorizedDataAccess(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - client := newAndroidClient(t, env) - - ownerAccount := testutil.NewRandomAccount(t) - maliciousAccount := testutil.NewRandomAccount(t) - - containerID := generateNewDataContainer(t, env, ownerAccount) - - req := &pushpb.AddTokenRequest{ - OwnerAccountId: maliciousAccount.ToProto(), - ContainerId: containerID.Proto(), - PushToken: memory_push.ValidAndroidPushToken, - TokenType: pushpb.TokenType_FCM_ANDROID, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := maliciousAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - _, err = client.AddToken(env.ctx, req) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) -} - -type testEnv struct { - ctx context.Context - target string - server *pushServer - data code_data.Provider -} - -func setup(t *testing.T) (env testEnv, cleanup func()) { - conn, serv, err := testutil.NewServer() - require.NoError(t, err) - - env.ctx = context.Background() - env.target = conn.Target() - env.data = code_data.NewTestDataProvider() - - s := NewPushServer(env.data, auth.NewRPCSignatureVerifier(env.data), memory_push.NewPushProvider()) - env.server = s.(*pushServer) - - serv.RegisterService(func(server *grpc.Server) { - pushpb.RegisterPushServer(server, s) - }) - - cleanup, err = serv.Serve() - require.NoError(t, err) - return env, cleanup -} - -func makeAddFcmAndroidTokenReq(t *testing.T, ownerAccount *common.Account, containerID user.DataContainerID) *pushpb.AddTokenRequest { - req := &pushpb.AddTokenRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - PushToken: memory_push.ValidAndroidPushToken, - AppInstall: &commonpb.AppInstallId{ - Value: uuid.NewString(), - }, - TokenType: pushpb.TokenType_FCM_ANDROID, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - return req -} - -func makeAddFcmApnsTokenReq(t *testing.T, ownerAccount *common.Account, containerID user.DataContainerID) *pushpb.AddTokenRequest { - req := &pushpb.AddTokenRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - PushToken: memory_push.ValidApplePushToken, - AppInstall: &commonpb.AppInstallId{ - Value: uuid.NewString(), - }, - TokenType: pushpb.TokenType_FCM_APNS, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - return req -} - -func makeAddReqWithInvalidToken(t *testing.T, ownerAccount *common.Account, containerID user.DataContainerID) *pushpb.AddTokenRequest { - req := &pushpb.AddTokenRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerID.Proto(), - PushToken: memory_push.InvalidPushToken, - TokenType: pushpb.TokenType_FCM_ANDROID, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - return req -} - -func generateNewDataContainer(t *testing.T, env testEnv, ownerAccount *common.Account) *user.DataContainerID { - phoneNumber := "+12223334444" - - container := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUserDataContainer(env.ctx, container)) - return container.ID -} - -// todo: integrate below client utilities with the main testutil package - -func newClientWithoutUserAgent(t *testing.T, env testEnv) pushpb.PushClient { - conn, err := grpc.Dial(env.target, grpc.WithInsecure()) - require.NoError(t, err) - - return pushpb.NewPushClient(conn) -} - -func newAndroidClient(t *testing.T, env testEnv) pushpb.PushClient { - conn, err := grpc.Dial(env.target, grpc.WithInsecure(), grpc.WithUserAgent("Code/Android/1.0.0")) - require.NoError(t, err) - - return pushpb.NewPushClient(conn) -} - -func newIOSClient(t *testing.T, env testEnv) pushpb.PushClient { - conn, err := grpc.Dial(env.target, grpc.WithInsecure(), grpc.WithUserAgent("Code/iOS/1.0.0")) - require.NoError(t, err) - - return pushpb.NewPushClient(conn) -} diff --git a/pkg/code/server/grpc/transaction/v2/action_handler.go b/pkg/code/server/grpc/transaction/v2/action_handler.go deleted file mode 100644 index dbab2b99..00000000 --- a/pkg/code/server/grpc/transaction/v2/action_handler.go +++ /dev/null @@ -1,877 +0,0 @@ -package transaction_v2 - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "time" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/fulfillment" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/merkletree" - "github.com/code-payments/code-server/pkg/code/data/timelock" - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/cvm" -) - -type newFulfillmentMetadata struct { - // Signature metadata - - requiresClientSignature bool - expectedSigner *common.Account // Must be null if the requiresClientSignature is false - virtualIxnHash *cvm.CompactMessage // Must be null if the requiresClientSignature is false - - // Additional metadata to add to the action and fulfillment record, which relates - // specifically to the transaction or virtual instruction within the context of - // the action. - - fulfillmentType fulfillment.Type - - source *common.Account - destination *common.Account - - fulfillmentOrderingIndex uint32 - disableActiveScheduling bool -} - -// BaseActionHandler is a base interface for operation-specific action handlers -// -// Note: Action handlers should load all required state on initialization to -// avoid duplicated work across interface method calls. -type BaseActionHandler interface { - // GetServerParameter gets the server parameter for the action within the context - // of the intent. - GetServerParameter() *transactionpb.ServerParameter - - // OnSaveToDB is a callback when the action is being saved to the DB - // within the scope of a DB transaction. Additional supporting DB records - // (ie. not the action or fulfillment records) relevant to the action should - // be saved here. - OnSaveToDB(ctx context.Context) error -} - -// CreateActionHandler is an interface for creating new actions -type CreateActionHandler interface { - BaseActionHandler - - // FulfillmentCount returns the total number of fulfillments that - // will be created for the action. - FulfillmentCount() int - - // PopulateMetadata populates action metadata into the provided record - PopulateMetadata(actionRecord *action.Record) error - - // RequiresNonce determines whether a nonce should be acquired for the - // fulfillment being created. This should be true whenever a virtual - // instruction needs to be signed by the client. - RequiresNonce(fulfillmentIndex int) bool - - // GetFulfillmentMetadata gets metadata for the fulfillment being created - GetFulfillmentMetadata( - index int, - nonce *common.Account, - bh solana.Blockhash, - ) (*newFulfillmentMetadata, error) -} - -// UpgradeActionHandler is an interface for upgrading existing actions. It's -// assumed we'll only be upgrading a single fulfillment. -type UpgradeActionHandler interface { - BaseActionHandler - - // GetFulfillmentBeingUpgraded gets the original fulfillment that's being - // upgraded. - GetFulfillmentBeingUpgraded() *fulfillment.Record - - // GetFulfillmentMetadata gets upgraded fulfillment metadata - GetFulfillmentMetadata( - nonce *common.Account, - bh solana.Blockhash, - ) (*newFulfillmentMetadata, error) -} - -type OpenAccountActionHandler struct { - data code_data.Provider - - accountType commonpb.AccountType - timelockAccounts *common.TimelockAccounts - - unsavedAccountInfoRecord *account.Record - unsavedTimelockRecord *timelock.Record -} - -func NewOpenAccountActionHandler(data code_data.Provider, protoAction *transactionpb.OpenAccountAction, protoMetadata *transactionpb.Metadata) (CreateActionHandler, error) { - owner, err := common.NewAccountFromProto(protoAction.Owner) - if err != nil { - return nil, err - } - - authority, err := common.NewAccountFromProto(protoAction.Authority) - if err != nil { - return nil, err - } - - timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - - var relationshipTo *string - switch typed := protoMetadata.Type.(type) { - case *transactionpb.Metadata_EstablishRelationship: - relationshipTo = &typed.EstablishRelationship.Relationship.GetDomain().Value - } - - unsavedAccountInfoRecord := &account.Record{ - OwnerAccount: owner.PublicKey().ToBase58(), - AuthorityAccount: authority.PublicKey().ToBase58(), - TokenAccount: timelockAccounts.Vault.PublicKey().ToBase58(), - MintAccount: timelockAccounts.Mint.PublicKey().ToBase58(), - AccountType: protoAction.AccountType, - Index: protoAction.Index, - RelationshipTo: relationshipTo, - DepositsLastSyncedAt: time.Now(), - RequiresDepositSync: false, - RequiresAutoReturnCheck: protoAction.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD, - } - - unsavedTimelockRecord := timelockAccounts.ToDBRecord() - - return &OpenAccountActionHandler{ - data: data, - - accountType: protoAction.AccountType, - timelockAccounts: timelockAccounts, - - unsavedAccountInfoRecord: unsavedAccountInfoRecord, - unsavedTimelockRecord: unsavedTimelockRecord, - }, nil -} - -func (h *OpenAccountActionHandler) FulfillmentCount() int { - return 1 -} - -func (h *OpenAccountActionHandler) PopulateMetadata(actionRecord *action.Record) error { - actionRecord.Source = h.timelockAccounts.Vault.PublicKey().ToBase58() - - actionRecord.State = action.StatePending - - return nil -} - -func (h *OpenAccountActionHandler) GetServerParameter() *transactionpb.ServerParameter { - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_OpenAccount{ - OpenAccount: &transactionpb.OpenAccountServerParameter{}, - }, - } -} - -func (h *OpenAccountActionHandler) RequiresNonce(index int) bool { - return false -} - -func (h *OpenAccountActionHandler) GetFulfillmentMetadata( - index int, - nonce *common.Account, - bh solana.Blockhash, -) (*newFulfillmentMetadata, error) { - switch index { - case 0: - return &newFulfillmentMetadata{ - requiresClientSignature: false, - expectedSigner: nil, - virtualIxnHash: nil, - - fulfillmentType: fulfillment.InitializeLockedTimelockAccount, - source: h.timelockAccounts.Vault, - destination: nil, - fulfillmentOrderingIndex: 0, - disableActiveScheduling: h.accountType != commonpb.AccountType_PRIMARY, // Non-primary accounts are created on demand after first usage - }, nil - default: - return nil, errors.New("invalid virtual ixn index") - } -} - -func (h *OpenAccountActionHandler) OnSaveToDB(ctx context.Context) error { - err := h.data.SaveTimelock(ctx, h.unsavedTimelockRecord) - if err != nil { - return err - } - - return h.data.CreateAccountInfo(ctx, h.unsavedAccountInfoRecord) -} - -type NoPrivacyTransferActionHandler struct { - source *common.TimelockAccounts - destination *common.Account - amount uint64 - isFeePayment bool // Internally, the mechanics of a fee payment are exactly the same - isCodeFeePayment bool -} - -func NewNoPrivacyTransferActionHandler(protoAction *transactionpb.NoPrivacyTransferAction) (CreateActionHandler, error) { - sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) - if err != nil { - return nil, err - } - - source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - - destination, err := common.NewAccountFromProto(protoAction.Destination) - if err != nil { - return nil, err - } - - return &NoPrivacyTransferActionHandler{ - source: source, - destination: destination, - amount: protoAction.Amount, - isFeePayment: false, - }, nil -} - -func NewFeePaymentActionHandler(protoAction *transactionpb.FeePaymentAction, feeCollector *common.Account) (CreateActionHandler, error) { - sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) - if err != nil { - return nil, err - } - - source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - - var destination *common.Account - var isCodeFeePayment bool - if protoAction.Type == transactionpb.FeePaymentAction_CODE { - destination = feeCollector - isCodeFeePayment = true - } else { - destination, err = common.NewAccountFromProto(protoAction.Destination) - if err != nil { - return nil, err - } - } - - return &NoPrivacyTransferActionHandler{ - source: source, - destination: destination, - amount: protoAction.Amount, - isFeePayment: true, - isCodeFeePayment: isCodeFeePayment, - }, nil -} - -func (h *NoPrivacyTransferActionHandler) FulfillmentCount() int { - return 1 -} - -func (h *NoPrivacyTransferActionHandler) PopulateMetadata(actionRecord *action.Record) error { - actionRecord.Source = h.source.Vault.PublicKey().ToBase58() - - destination := h.destination.PublicKey().ToBase58() - actionRecord.Destination = &destination - - actionRecord.Quantity = &h.amount - - actionRecord.State = action.StatePending - - return nil -} -func (h *NoPrivacyTransferActionHandler) GetServerParameter() *transactionpb.ServerParameter { - if h.isFeePayment { - var codeDestination *commonpb.SolanaAccountId - if h.isCodeFeePayment { - codeDestination = h.destination.ToProto() - } - - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_FeePayment{ - FeePayment: &transactionpb.FeePaymentServerParameter{ - CodeDestination: codeDestination, - }, - }, - } - } - - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_NoPrivacyTransfer{ - NoPrivacyTransfer: &transactionpb.NoPrivacyTransferServerParameter{}, - }, - } -} - -func (h *NoPrivacyTransferActionHandler) RequiresNonce(index int) bool { - return true -} - -func (h *NoPrivacyTransferActionHandler) GetFulfillmentMetadata( - index int, - nonce *common.Account, - bh solana.Blockhash, -) (*newFulfillmentMetadata, error) { - switch index { - case 0: - virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ - Source: h.source.Vault.PublicKey().ToBytes(), - Destination: h.destination.PublicKey().ToBytes(), - Amount: h.amount, - NonceAddress: nonce.PublicKey().ToBytes(), - NonceValue: cvm.Hash(bh), - }) - - return &newFulfillmentMetadata{ - requiresClientSignature: true, - expectedSigner: h.source.VaultOwner, - virtualIxnHash: &virtualIxnHash, - - fulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, - source: h.source.Vault, - destination: h.destination, - fulfillmentOrderingIndex: 0, - disableActiveScheduling: h.isFeePayment, - }, nil - default: - return nil, errors.New("invalid transaction index") - } -} - -func (h *NoPrivacyTransferActionHandler) OnSaveToDB(ctx context.Context) error { - return nil -} - -type NoPrivacyWithdrawActionHandler struct { - source *common.TimelockAccounts - destination *common.Account - amount uint64 - disableActiveScheduling bool -} - -func NewNoPrivacyWithdrawActionHandler(intentRecord *intent.Record, protoAction *transactionpb.NoPrivacyWithdrawAction) (CreateActionHandler, error) { - var disableActiveScheduling bool - - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - // Technically we should do this for public receives too, but we don't - // yet have a great way of doing cross intent fulfillment polling hints. - disableActiveScheduling = true - } - - sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) - if err != nil { - return nil, err - } - - source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - - destination, err := common.NewAccountFromProto(protoAction.Destination) - if err != nil { - return nil, err - } - - return &NoPrivacyWithdrawActionHandler{ - source: source, - destination: destination, - amount: protoAction.Amount, - disableActiveScheduling: disableActiveScheduling, - }, nil -} - -func (h *NoPrivacyWithdrawActionHandler) FulfillmentCount() int { - return 1 -} - -func (h *NoPrivacyWithdrawActionHandler) PopulateMetadata(actionRecord *action.Record) error { - actionRecord.Source = h.source.Vault.PublicKey().ToBase58() - - destination := h.destination.PublicKey().ToBase58() - actionRecord.Destination = &destination - - actionRecord.Quantity = &h.amount - - actionRecord.State = action.StatePending - - return nil -} -func (h *NoPrivacyWithdrawActionHandler) GetServerParameter() *transactionpb.ServerParameter { - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_NoPrivacyWithdraw{ - NoPrivacyWithdraw: &transactionpb.NoPrivacyWithdrawServerParameter{}, - }, - } -} - -func (h *NoPrivacyWithdrawActionHandler) RequiresNonce(index int) bool { - return true -} - -func (h *NoPrivacyWithdrawActionHandler) GetFulfillmentMetadata( - index int, - nonce *common.Account, - bh solana.Blockhash, -) (*newFulfillmentMetadata, error) { - switch index { - case 0: - virtualIxnHash := cvm.GetCompactWithdrawMessage(&cvm.GetCompactWithdrawMessageArgs{ - Source: h.source.Vault.PublicKey().ToBytes(), - Destination: h.destination.PublicKey().ToBytes(), - NonceAddress: nonce.PublicKey().ToBytes(), - NonceValue: cvm.Hash(bh), - }) - - return &newFulfillmentMetadata{ - requiresClientSignature: true, - expectedSigner: h.source.VaultOwner, - virtualIxnHash: &virtualIxnHash, - - fulfillmentType: fulfillment.NoPrivacyWithdraw, - source: h.source.Vault, - destination: h.destination, - fulfillmentOrderingIndex: 0, - - disableActiveScheduling: h.disableActiveScheduling, - }, nil - default: - return nil, errors.New("invalid transaction index") - } -} - -func (h *NoPrivacyWithdrawActionHandler) OnSaveToDB(ctx context.Context) error { - return nil -} - -// Handles both of the equivalent client transfer and exchange actions. The -// server-defined action only defines the private movement of funds between -// accounts and it's all treated the same by backend processes. The client -// definitions are merely metadata to tell us more about the reasoning of -// the movement of funds. -type TemporaryPrivacyTransferActionHandler struct { - data code_data.Provider - - source *common.TimelockAccounts - destination *common.Account - treasuryPool *common.Account - treasuryPoolVault *common.Account - commitment *common.Account - commitmentVault *common.Account - - recentRoot merkletree.Hash - transcript []byte - - unsavedCommitmentRecord *commitment.Record - - isExchange bool - - isCollectedForHideInTheCrowdPrivacy bool -} - -func NewTemporaryPrivacyTransferActionHandler( - ctx context.Context, - conf *conf, - data code_data.Provider, - intentRecord *intent.Record, - untypedAction *transactionpb.Action, - isExchange bool, - treasuryPoolSelector func(context.Context, uint64) (string, error), -) (CreateActionHandler, error) { - var authorityProto *commonpb.SolanaAccountId - var destinationProto *commonpb.SolanaAccountId - var amount uint64 - isCollectedForHideInTheCrowdPrivacy := true - if isExchange { - typedAction := untypedAction.GetTemporaryPrivacyExchange() - if typedAction == nil { - return nil, errors.New("invalid proto action") - } - authorityProto = typedAction.Authority - destinationProto = typedAction.Destination - amount = typedAction.Amount - } else { - typedAction := untypedAction.GetTemporaryPrivacyTransfer() - if typedAction == nil { - return nil, errors.New("invalid proto action") - } - authorityProto = typedAction.Authority - destinationProto = typedAction.Destination - amount = typedAction.Amount - - // Private payment withdrawals bypass collection state for hide in the - // crowd privacy. They need to be sent immediately to fulfill withdrawal - // requirements. - if intentRecord.IntentType == intent.SendPrivatePayment && intentRecord.SendPrivatePaymentMetadata.IsWithdrawal { - isCollectedForHideInTheCrowdPrivacy = false - } - } - - h := &TemporaryPrivacyTransferActionHandler{ - data: data, - isExchange: isExchange, - isCollectedForHideInTheCrowdPrivacy: isCollectedForHideInTheCrowdPrivacy, - } - - authority, err := common.NewAccountFromProto(authorityProto) - if err != nil { - return nil, err - } - - h.source, err = authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - - h.destination, err = common.NewAccountFromProto(destinationProto) - if err != nil { - return nil, err - } - - selectedTreasuryPoolName, err := treasuryPoolSelector(ctx, amount) - if err != nil { - return nil, err - } - - cachedTreasuryMetadata, err := getCachedTreasuryMetadataByNameOrAddress(ctx, h.data, selectedTreasuryPoolName, conf.treasuryPoolRecentRootCacheMaxAge.Get(ctx)) - if err != nil { - return nil, err - } - - h.treasuryPool = cachedTreasuryMetadata.stateAccount - h.treasuryPoolVault = cachedTreasuryMetadata.vaultAccount - - h.recentRoot, err = hex.DecodeString(cachedTreasuryMetadata.mostRecentRoot) - if err != nil { - return nil, err - } - - h.transcript = getTransript( - intentRecord.IntentId, - untypedAction.Id, - h.source.Vault, - h.destination, - amount, - ) - - commitmentAddress, _, err := cvm.GetRelayCommitmentAddress(&cvm.GetRelayCommitmentAddressArgs{ - Relay: h.treasuryPool.PublicKey().ToBytes(), - MerkleRoot: cvm.Hash(h.recentRoot), - Transcript: cvm.Hash(h.transcript), - Destination: h.destination.PublicKey().ToBytes(), - Amount: amount, - }) - if err != nil { - return nil, err - } - h.commitment, err = common.NewAccountFromPublicKeyBytes(commitmentAddress) - if err != nil { - return nil, err - } - - proofAddress, _, err := cvm.GetRelayProofAddress(&cvm.GetRelayProofAddressArgs{ - Relay: h.treasuryPool.PublicKey().ToBytes(), - MerkleRoot: cvm.Hash(h.recentRoot), - Commitment: cvm.Hash(commitmentAddress), - }) - if err != nil { - return nil, err - } - - commitmentVaultAddress, _, err := cvm.GetRelayDestinationAddress(&cvm.GetRelayDestinationAddressArgs{ - RelayOrProof: proofAddress, - }) - if err != nil { - return nil, err - } - h.commitmentVault, err = common.NewAccountFromPublicKeyBytes(commitmentVaultAddress) - if err != nil { - return nil, err - } - - h.unsavedCommitmentRecord = &commitment.Record{ - Address: h.commitment.PublicKey().ToBase58(), - VaultAddress: h.commitmentVault.PublicKey().ToBase58(), - - Pool: h.treasuryPool.PublicKey().ToBase58(), - RecentRoot: cachedTreasuryMetadata.mostRecentRoot, - - Transcript: hex.EncodeToString(h.transcript), - Destination: h.destination.PublicKey().ToBase58(), - Amount: amount, - - Intent: intentRecord.IntentId, - ActionId: untypedAction.Id, - - Owner: intentRecord.InitiatorOwnerAccount, - - State: commitment.StateUnknown, - } - - return h, nil -} - -func (h *TemporaryPrivacyTransferActionHandler) FulfillmentCount() int { - return 2 -} - -func (h *TemporaryPrivacyTransferActionHandler) PopulateMetadata(actionRecord *action.Record) error { - actionRecord.Source = h.source.Vault.PublicKey().ToBase58() - actionRecord.Destination = &h.unsavedCommitmentRecord.Destination - actionRecord.Quantity = &h.unsavedCommitmentRecord.Amount - - actionRecord.State = action.StatePending - - return nil -} - -func (h *TemporaryPrivacyTransferActionHandler) GetServerParameter() *transactionpb.ServerParameter { - if h.isExchange { - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_TemporaryPrivacyExchange{ - TemporaryPrivacyExchange: &transactionpb.TemporaryPrivacyExchangeServerParameter{ - Treasury: h.treasuryPool.ToProto(), - RecentRoot: &commonpb.Hash{ - Value: h.recentRoot, - }, - }, - }, - } - } - - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_TemporaryPrivacyTransfer{ - TemporaryPrivacyTransfer: &transactionpb.TemporaryPrivacyTransferServerParameter{ - Treasury: h.treasuryPool.ToProto(), - RecentRoot: &commonpb.Hash{ - Value: h.recentRoot, - }, - }, - }, - } -} - -func (h *TemporaryPrivacyTransferActionHandler) RequiresNonce(index int) bool { - return index != 0 -} - -func (h *TemporaryPrivacyTransferActionHandler) GetFulfillmentMetadata( - index int, - nonce *common.Account, - bh solana.Blockhash, -) (*newFulfillmentMetadata, error) { - switch index { - case 0: - return &newFulfillmentMetadata{ - requiresClientSignature: false, - expectedSigner: nil, - virtualIxnHash: nil, - - fulfillmentType: fulfillment.TransferWithCommitment, - source: h.treasuryPoolVault, - destination: h.destination, - fulfillmentOrderingIndex: 0, - disableActiveScheduling: h.isCollectedForHideInTheCrowdPrivacy, - }, nil - case 1: - virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ - Source: h.source.Vault.PublicKey().ToBytes(), - Destination: h.commitmentVault.PublicKey().ToBytes(), - Amount: h.unsavedCommitmentRecord.Amount, - NonceAddress: nonce.PublicKey().ToBytes(), - NonceValue: cvm.Hash(bh), - }) - - return &newFulfillmentMetadata{ - requiresClientSignature: true, - expectedSigner: h.source.VaultOwner, - virtualIxnHash: &virtualIxnHash, - - fulfillmentType: fulfillment.TemporaryPrivacyTransferWithAuthority, - source: h.source.Vault, - destination: h.commitmentVault, // Technically treasury vault with VM, but would break a number of things that we don't want to deal with for now - fulfillmentOrderingIndex: 2000, - disableActiveScheduling: true, - }, nil - default: - return nil, errors.New("invalid transaction index") - } -} - -func (h *TemporaryPrivacyTransferActionHandler) OnSaveToDB(ctx context.Context) error { - return h.data.SaveCommitment(ctx, h.unsavedCommitmentRecord) -} - -// Handles both of the equivalent client transfer and exchange actions. The -// server-defined action only defines the private movement of funds between -// accounts and it's all treated the same by backend processes. The client -// definitions are merely metadata to tell us more about the reasoning of -// the movement of funds. -type PermanentPrivacyUpgradeActionHandler struct { - data code_data.Provider - - source *common.TimelockAccounts - commitmentBeingUpgraded *commitment.Record - privacyUpgradeProof *privacyUpgradeProof - - fulfillmentToUpgrade *fulfillment.Record -} - -func NewPermanentPrivacyUpgradeActionHandler( - ctx context.Context, - data code_data.Provider, - intentRecord *intent.Record, - protoAction *transactionpb.PermanentPrivacyUpgradeAction, - cachedUpgradeTarget *privacyUpgradeCandidate, -) (UpgradeActionHandler, error) { - h := &PermanentPrivacyUpgradeActionHandler{ - data: data, - } - - var err error - h.fulfillmentToUpgrade, err = h.getFulfillmentBeingUpgraded(ctx, intentRecord, protoAction) - if err != nil { - return nil, err - } - - h.commitmentBeingUpgraded, err = h.data.GetCommitmentByAction(ctx, intentRecord.IntentId, protoAction.ActionId) - if err != nil { - return nil, err - } - - h.privacyUpgradeProof, err = getProofForPrivacyUpgrade(ctx, h.data, cachedUpgradeTarget) - if err != nil { - return nil, err - } - - actionRecord, err := h.data.GetActionById(ctx, intentRecord.IntentId, protoAction.ActionId) - if err != nil { - return nil, err - } - - sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, actionRecord.Source) - if err != nil { - return nil, err - } - - authority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) - if err != nil { - return nil, err - } - - h.source, err = authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - - return h, nil -} - -func (h *PermanentPrivacyUpgradeActionHandler) GetServerParameter() *transactionpb.ServerParameter { - protoProof := make([]*commonpb.Hash, len(h.privacyUpgradeProof.proof)) - for i, hash := range h.privacyUpgradeProof.proof { - protoProof[i] = &commonpb.Hash{ - Value: hash, - } - } - - return &transactionpb.ServerParameter{ - Type: &transactionpb.ServerParameter_PermanentPrivacyUpgrade{ - PermanentPrivacyUpgrade: &transactionpb.PermanentPrivacyUpgradeServerParameter{ - NewCommitment: h.privacyUpgradeProof.newCommitment.ToProto(), - NewCommitmentTranscript: &commonpb.Hash{ - Value: h.privacyUpgradeProof.newCommitmentTranscript, - }, - NewCommitmentDestination: h.privacyUpgradeProof.newCommitmentDestination.ToProto(), - NewCommitmentAmount: h.privacyUpgradeProof.newCommitmentAmount, - MerkleRoot: &commonpb.Hash{ - Value: h.privacyUpgradeProof.newCommitmentRoot, - }, - MerkleProof: protoProof, - }, - }, - } -} - -func (h *PermanentPrivacyUpgradeActionHandler) GetFulfillmentBeingUpgraded() *fulfillment.Record { - return h.fulfillmentToUpgrade -} - -func (h *PermanentPrivacyUpgradeActionHandler) getFulfillmentBeingUpgraded(ctx context.Context, intentRecord *intent.Record, protoAction *transactionpb.PermanentPrivacyUpgradeAction) (*fulfillment.Record, error) { - fulfillmentRecords, err := h.data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, intentRecord.IntentId, protoAction.ActionId) - if err != nil { - return nil, err - } - - if len(fulfillmentRecords) != 1 { - return nil, errors.New("fulfillment to upgrade was not found") - } - - return fulfillmentRecords[0], nil -} - -func (h *PermanentPrivacyUpgradeActionHandler) GetFulfillmentMetadata( - nonce *common.Account, - bh solana.Blockhash, -) (*newFulfillmentMetadata, error) { - virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ - Source: h.source.Vault.PublicKey().ToBytes(), - Destination: h.privacyUpgradeProof.newCommitmentVault.PublicKey().ToBytes(), - Amount: h.commitmentBeingUpgraded.Amount, - NonceAddress: nonce.PublicKey().ToBytes(), - NonceValue: cvm.Hash(bh), - }) - - return &newFulfillmentMetadata{ - requiresClientSignature: true, - expectedSigner: h.source.VaultOwner, - virtualIxnHash: &virtualIxnHash, - - fulfillmentType: fulfillment.PermanentPrivacyTransferWithAuthority, - source: h.source.Vault, - destination: h.privacyUpgradeProof.newCommitmentVault, // Technically treasury vault with VM, but would break a number of things that we don't want to deal with for now - fulfillmentOrderingIndex: 1000, - }, nil -} - -func (h *PermanentPrivacyUpgradeActionHandler) OnSaveToDB(ctx context.Context) error { - newDestination := h.privacyUpgradeProof.newCommitmentVault.PublicKey().ToBase58() - h.commitmentBeingUpgraded.RepaymentDivertedTo = &newDestination - return h.data.SaveCommitment(ctx, h.commitmentBeingUpgraded) -} - -func getTransript( - intent string, - action uint32, - source *common.Account, - destination *common.Account, - kinAmountInQuarks uint64, -) []byte { - transcript := fmt.Sprintf( - "receipt[%s, %d]: transfer %d quarks from %s to %s", - intent, - action, - kinAmountInQuarks, - source.PublicKey().ToBase58(), - destination.PublicKey().ToBase58(), - ) - - hasher := sha256.New() - hasher.Write([]byte(transcript)) - return hasher.Sum(nil) -} diff --git a/pkg/code/server/grpc/transaction/v2/config.go b/pkg/code/server/grpc/transaction/v2/config.go deleted file mode 100644 index 1805483d..00000000 --- a/pkg/code/server/grpc/transaction/v2/config.go +++ /dev/null @@ -1,165 +0,0 @@ -package transaction_v2 - -import ( - "time" - - "github.com/code-payments/code-server/pkg/config" - "github.com/code-payments/code-server/pkg/config/env" - "github.com/code-payments/code-server/pkg/config/memory" - "github.com/code-payments/code-server/pkg/config/wrapper" -) - -const ( - envConfigPrefix = "TRANSACTION_V2_SERVICE_" - - DisableSubmitIntentConfigEnvName = envConfigPrefix + "DISABLE_SUBMIT_INTENT" - defaultDisableSubmitIntent = false - - DisableBlockchainChecksConfigEnvName = envConfigPrefix + "DISABLE_BLOCKCHAIN_CHECKS" - defaultDisableBlockchainChecks = false - - SubmitIntentTimeoutConfigEnvName = envConfigPrefix + "SUBMIT_INTENT_TIMEOUT" - defaultSubmitIntentTimeout = 5 * time.Second - - SwapTimeoutConfigEnvName = envConfigPrefix + "SWAP_TIMEOUT" - defaultSwapTimeout = 60 * time.Second - - SwapPriorityFeeMultiple = envConfigPrefix + "SWAP_PRIORITY_FEE_MULTIPLE" - defaultSwapPriorityFeeMultiple = 1.0 - - ClientReceiveTimeoutConfigEnvName = envConfigPrefix + "CLIENT_RECEIVE_TIMEOUT" - defaultClientReceiveTimeout = time.Second - - FeeCollectorTokenPublicKeyConfigEnvName = envConfigPrefix + "FEE_COLLECTOR_TOKEN_PUBLIC_KEY" - defaultFeeCollectorPublicKey = "invalid" // Ensure something valid is set - - EnableAirdropsConfigEnvName = envConfigPrefix + "ENABLE_AIRDROPS" - defaultEnableAirdrops = false - - AirdropperOwnerPublicKeyEnvName = envConfigPrefix + "AIRDROPPER_OWNER_PUBLIC_KEY" - defaultAirdropperOwnerPublicKey = "invalid" // Ensure something valid is set - - SwapSubsidizerOwnerPublicKeyEnvName = envConfigPrefix + "SWAP_SUBSIDIZER_OWNER_PUBLIC_KEY" - defaultSwapSubsidizerOwnerPublicKey = "invalid" // Ensure something valid is set - - TreasuryPoolOneKinBucketConfigEnvName = envConfigPrefix + "TREASURY_POOL_1_KIN_BUCKET" - TreasuryPoolTenKinBucketConfigEnvName = envConfigPrefix + "TREASURY_POOL_10_KIN_BUCKET" - TreasuryPoolHundredKinBucketConfigEnvName = envConfigPrefix + "TREASURY_POOL_100_KIN_BUCKET" - TreasuryPoolThousandKinBucketConfigEnvName = envConfigPrefix + "TREASURY_POOL_1_000_KIN_BUCKET" - TreasuryPoolTenThosandKinBucketConfigEnvName = envConfigPrefix + "TREASURY_POOL_10_000_KIN_BUCKET" - TreasuryPoolHundredThousandKinBucketConfigEnvName = envConfigPrefix + "TREASURY_POOL_100_000_KIN_BUCKET" - TreasuryPoolMillionKinBucketConfigEnvName = envConfigPrefix + "TREASURY_POOL_1_000_000_KIN_BUCKET" - defaultTreasuryPoolName = "invalid" // Ensure something valid is set - - TreasuryPoolRecentRootCacheMaxAgeEnvName = envConfigPrefix + "TREASURY_POOL_RECENT_ROOT_CACHE_MAX_AGE" - defaultTreasuryPoolRecentRootCacheMaxAge = time.Second - - TreasuryPoolStatsRefreshIntervalEnvName = envConfigPrefix + "TREASURY_POOL_STATS_REFRESH_INTERVAL" - defaultTreasuryPoolStatsRefreshInterval = time.Second -) - -type conf struct { - disableSubmitIntent config.Bool - disableAntispamChecks config.Bool // To avoid limits during testing - disableAmlChecks config.Bool // To avoid limits during testing - disableBlockchainChecks config.Bool - submitIntentTimeout config.Duration - swapTimeout config.Duration - clientReceiveTimeout config.Duration - feeCollectorTokenPublicKey config.String - enableAirdrops config.Bool - enableAsyncAirdropProcessing config.Bool - airdropperOwnerPublicKey config.String - swapSubsidizerOwnerPublicKey config.String - swapPriorityFeeMultiple config.Float64 - treasuryPoolOneKinBucket config.String - treasuryPoolTenKinBucket config.String - treasuryPoolHundredKinBucket config.String - treasuryPoolThousandKinBucket config.String - treasuryPoolTenThousandKinBucket config.String - treasuryPoolHundredThousandKinBucket config.String - treasuryPoolMillionKinBucket config.String - treasuryPoolRecentRootCacheMaxAge config.Duration - treasuryPoolStatsRefreshInterval config.Duration - stripedLockParallelization config.Uint64 -} - -// ConfigProvider defines how config values are pulled -type ConfigProvider func() *conf - -// WithEnvConfigs returns configuration pulled from environment variables -func WithEnvConfigs() ConfigProvider { - return func() *conf { - return &conf{ - disableSubmitIntent: env.NewBoolConfig(DisableSubmitIntentConfigEnvName, defaultDisableSubmitIntent), - disableAntispamChecks: wrapper.NewBoolConfig(memory.NewConfig(false), false), - disableAmlChecks: wrapper.NewBoolConfig(memory.NewConfig(false), false), - disableBlockchainChecks: env.NewBoolConfig(DisableBlockchainChecksConfigEnvName, defaultDisableBlockchainChecks), - submitIntentTimeout: env.NewDurationConfig(SubmitIntentTimeoutConfigEnvName, defaultSubmitIntentTimeout), - swapTimeout: env.NewDurationConfig(SwapTimeoutConfigEnvName, defaultSwapTimeout), - clientReceiveTimeout: env.NewDurationConfig(ClientReceiveTimeoutConfigEnvName, defaultClientReceiveTimeout), - feeCollectorTokenPublicKey: env.NewStringConfig(FeeCollectorTokenPublicKeyConfigEnvName, defaultFeeCollectorPublicKey), - enableAirdrops: env.NewBoolConfig(EnableAirdropsConfigEnvName, defaultEnableAirdrops), - enableAsyncAirdropProcessing: wrapper.NewBoolConfig(memory.NewConfig(true), true), - airdropperOwnerPublicKey: env.NewStringConfig(AirdropperOwnerPublicKeyEnvName, defaultAirdropperOwnerPublicKey), - swapSubsidizerOwnerPublicKey: env.NewStringConfig(SwapSubsidizerOwnerPublicKeyEnvName, defaultSwapSubsidizerOwnerPublicKey), - swapPriorityFeeMultiple: env.NewFloat64Config(SwapPriorityFeeMultiple, defaultSwapPriorityFeeMultiple), - treasuryPoolOneKinBucket: env.NewStringConfig(TreasuryPoolOneKinBucketConfigEnvName, defaultTreasuryPoolName), - treasuryPoolTenKinBucket: env.NewStringConfig(TreasuryPoolTenKinBucketConfigEnvName, defaultTreasuryPoolName), - treasuryPoolHundredKinBucket: env.NewStringConfig(TreasuryPoolHundredKinBucketConfigEnvName, defaultTreasuryPoolName), - treasuryPoolThousandKinBucket: env.NewStringConfig(TreasuryPoolThousandKinBucketConfigEnvName, defaultTreasuryPoolName), - treasuryPoolTenThousandKinBucket: env.NewStringConfig(TreasuryPoolTenThosandKinBucketConfigEnvName, defaultTreasuryPoolName), - treasuryPoolHundredThousandKinBucket: env.NewStringConfig(TreasuryPoolHundredThousandKinBucketConfigEnvName, defaultTreasuryPoolName), - treasuryPoolMillionKinBucket: env.NewStringConfig(TreasuryPoolMillionKinBucketConfigEnvName, defaultTreasuryPoolName), - treasuryPoolRecentRootCacheMaxAge: env.NewDurationConfig(TreasuryPoolRecentRootCacheMaxAgeEnvName, defaultTreasuryPoolRecentRootCacheMaxAge), - treasuryPoolStatsRefreshInterval: env.NewDurationConfig(TreasuryPoolStatsRefreshIntervalEnvName, defaultTreasuryPoolStatsRefreshInterval), - stripedLockParallelization: wrapper.NewUint64Config(memory.NewConfig(8192), 8192), - } - } -} - -type testOverrides struct { - disableSubmitIntent bool - enableAntispamChecks bool - enableAmlChecks bool - enableAirdrops bool - clientReceiveTimeout time.Duration - feeCollectorTokenPublicKey string - treasuryPoolOneKinBucket string - treasuryPoolTenKinBucket string - treasuryPoolHundredKinBucket string - treasuryPoolThousandKinBucket string - treasuryPoolTenThousandKinBucket string - treasuryPoolHundredThousandKinBucket string - treasuryPoolMillionKinBucket string -} - -func withManualTestOverrides(overrides *testOverrides) ConfigProvider { - return func() *conf { - return &conf{ - disableSubmitIntent: wrapper.NewBoolConfig(memory.NewConfig(overrides.disableSubmitIntent), defaultDisableSubmitIntent), - disableAntispamChecks: wrapper.NewBoolConfig(memory.NewConfig(!overrides.enableAntispamChecks), false), - disableAmlChecks: wrapper.NewBoolConfig(memory.NewConfig(!overrides.enableAmlChecks), false), - disableBlockchainChecks: wrapper.NewBoolConfig(memory.NewConfig(true), true), - submitIntentTimeout: wrapper.NewDurationConfig(memory.NewConfig(defaultSubmitIntentTimeout), defaultSubmitIntentTimeout), - swapTimeout: wrapper.NewDurationConfig(memory.NewConfig(defaultSwapTimeout), defaultSwapTimeout), - clientReceiveTimeout: wrapper.NewDurationConfig(memory.NewConfig(overrides.clientReceiveTimeout), defaultClientReceiveTimeout), - feeCollectorTokenPublicKey: wrapper.NewStringConfig(memory.NewConfig(overrides.feeCollectorTokenPublicKey), defaultFeeCollectorPublicKey), - enableAirdrops: wrapper.NewBoolConfig(memory.NewConfig(overrides.enableAirdrops), false), - enableAsyncAirdropProcessing: wrapper.NewBoolConfig(memory.NewConfig(false), false), - airdropperOwnerPublicKey: wrapper.NewStringConfig(memory.NewConfig(defaultAirdropperOwnerPublicKey), defaultAirdropperOwnerPublicKey), - swapSubsidizerOwnerPublicKey: wrapper.NewStringConfig(memory.NewConfig(defaultSwapSubsidizerOwnerPublicKey), defaultSwapSubsidizerOwnerPublicKey), - swapPriorityFeeMultiple: wrapper.NewFloat64Config(memory.NewConfig(defaultSwapPriorityFeeMultiple), defaultSwapPriorityFeeMultiple), - treasuryPoolOneKinBucket: wrapper.NewStringConfig(memory.NewConfig(overrides.treasuryPoolOneKinBucket), defaultTreasuryPoolName), - treasuryPoolTenKinBucket: wrapper.NewStringConfig(memory.NewConfig(overrides.treasuryPoolTenKinBucket), defaultTreasuryPoolName), - treasuryPoolHundredKinBucket: wrapper.NewStringConfig(memory.NewConfig(overrides.treasuryPoolHundredKinBucket), defaultTreasuryPoolName), - treasuryPoolThousandKinBucket: wrapper.NewStringConfig(memory.NewConfig(overrides.treasuryPoolThousandKinBucket), defaultTreasuryPoolName), - treasuryPoolTenThousandKinBucket: wrapper.NewStringConfig(memory.NewConfig(overrides.treasuryPoolTenThousandKinBucket), defaultTreasuryPoolName), - treasuryPoolHundredThousandKinBucket: wrapper.NewStringConfig(memory.NewConfig(overrides.treasuryPoolHundredThousandKinBucket), defaultTreasuryPoolName), - treasuryPoolMillionKinBucket: wrapper.NewStringConfig(memory.NewConfig(overrides.treasuryPoolMillionKinBucket), defaultTreasuryPoolName), - treasuryPoolRecentRootCacheMaxAge: wrapper.NewDurationConfig(memory.NewConfig(0), 0), - treasuryPoolStatsRefreshInterval: wrapper.NewDurationConfig(memory.NewConfig(50*time.Millisecond), 50*time.Millisecond), - stripedLockParallelization: wrapper.NewUint64Config(memory.NewConfig(4), 4), - } - } -} diff --git a/pkg/code/server/grpc/transaction/v2/intent_handler.go b/pkg/code/server/grpc/transaction/v2/intent_handler.go deleted file mode 100644 index cc2e92e7..00000000 --- a/pkg/code/server/grpc/transaction/v2/intent_handler.go +++ /dev/null @@ -1,2914 +0,0 @@ -package transaction_v2 - -import ( - "bytes" - "context" - "math" - "strings" - "time" - - "github.com/mr-tron/base58/base58" - "github.com/oschwald/maxminddb-golang" - "github.com/pkg/errors" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - "github.com/code-payments/code-server/pkg/code/antispam" - "github.com/code-payments/code-server/pkg/code/balance" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/event" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/paymentrequest" - "github.com/code-payments/code-server/pkg/code/data/twitter" - event_util "github.com/code-payments/code-server/pkg/code/event" - exchange_rate_util "github.com/code-payments/code-server/pkg/code/exchangerate" - "github.com/code-payments/code-server/pkg/code/lawenforcement" - "github.com/code-payments/code-server/pkg/code/thirdparty" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - push_lib "github.com/code-payments/code-server/pkg/push" - "github.com/code-payments/code-server/pkg/solana" -) - -var accountTypesToOpen = []commonpb.AccountType{ - commonpb.AccountType_PRIMARY, - commonpb.AccountType_TEMPORARY_INCOMING, - commonpb.AccountType_TEMPORARY_OUTGOING, - commonpb.AccountType_BUCKET_1_KIN, - commonpb.AccountType_BUCKET_10_KIN, - commonpb.AccountType_BUCKET_100_KIN, - commonpb.AccountType_BUCKET_1_000_KIN, - commonpb.AccountType_BUCKET_10_000_KIN, - commonpb.AccountType_BUCKET_100_000_KIN, - commonpb.AccountType_BUCKET_1_000_000_KIN, -} - -var bucketSizeByAccountType = map[commonpb.AccountType]uint64{ - commonpb.AccountType_BUCKET_1_KIN: kin.ToQuarks(1), - commonpb.AccountType_BUCKET_10_KIN: kin.ToQuarks(10), - commonpb.AccountType_BUCKET_100_KIN: kin.ToQuarks(100), - commonpb.AccountType_BUCKET_1_000_KIN: kin.ToQuarks(1_000), - commonpb.AccountType_BUCKET_10_000_KIN: kin.ToQuarks(10_000), - commonpb.AccountType_BUCKET_100_000_KIN: kin.ToQuarks(100_000), - commonpb.AccountType_BUCKET_1_000_000_KIN: kin.ToQuarks(1_000_000), -} - -var allowedBucketQuarkAmounts map[uint64]struct{} - -func init() { - allowedBucketQuarkAmounts = make(map[uint64]struct{}) - for _, bucketSize := range bucketSizeByAccountType { - for multiple := uint64(1); multiple < 10; multiple++ { - allowedBucketQuarkAmounts[multiple*bucketSize] = struct{}{} - } - } -} - -type lockableAccounts struct { - DestinationOwner *common.Account - RemoteSendGiftCardVault *common.Account -} - -// CreateIntentHandler is an interface for handling new intent creations -type CreateIntentHandler interface { - // PopulateMetadata adds intent metadata to the provided intent record - // using the client-provided protobuf variant. No other fields in the - // intent should be modified. - PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error - - // IsNoop determines whether the intent is a no-op operation. SubmitIntent will - // simply return OK and stop any further intent processing. - // - // Note: This occurs before validation, so if anything appears out-of-order, then - // the recommendation is to return false and have the verification logic catch the - // error. - IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) - - // GetAdditionalAccountsToLock gets additional accounts to apply distributed - // locking on that are specific to an intent. - // - // Note: Assumes relevant information is contained in the intent record after - // calling PopulateMetadata. - GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) - - // AllowCreation determines whether the new intent creation should be allowed. - AllowCreation(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action, deviceToken *string) error - - // OnSaveToDB is a callback when the intent is being saved to the DB - // within the scope of a DB transaction. Additional supporting DB records - // (ie. not the intent record) relevant to the intent should be saved here. - OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error - - // OnCommittedToDB is a callback when the intent has been committed to the - // DB. Any instant side-effects should called here, and can be done async - // in a new goroutine to not affect SubmitIntent latency. - // - // Note: Any errors generated here have no effect on rolling back the intent. - // This is all best-effort up to this point. Use a worker for things - // requiring retries! - OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error -} - -// UpdateIntentHandler is an interface for handling updates to an existing intent -type UpdateIntentHandler interface { - // AllowUpdate determines whether an intent update should be allowed. - AllowUpdate(ctx context.Context, existingIntent *intent.Record, metdata *transactionpb.Metadata, actions []*transactionpb.Action) error -} - -type OpenAccountsIntentHandler struct { - conf *conf - data code_data.Provider - antispamGuard *antispam.Guard - antispamSuccessCallback func() error - maxmind *maxminddb.Reader -} - -func NewOpenAccountsIntentHandler(conf *conf, data code_data.Provider, antispamGuard *antispam.Guard, maxmind *maxminddb.Reader) CreateIntentHandler { - return &OpenAccountsIntentHandler{ - conf: conf, - data: data, - antispamGuard: antispamGuard, - maxmind: maxmind, - } -} - -func (h *OpenAccountsIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { - typedProtoMetadata := protoMetadata.GetOpenAccounts() - if typedProtoMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - intentRecord.IntentType = intent.OpenAccounts - intentRecord.OpenAccountsMetadata = &intent.OpenAccountsMetadata{} - - return nil -} - -func (h *OpenAccountsIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { - initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - return false, err - } - - _, err = h.data.GetLatestIntentByInitiatorAndType(ctx, intent.OpenAccounts, initiatiorOwnerAccount.PublicKey().ToBase58()) - if err == nil { - return true, nil - } else if err != intent.ErrIntentNotFound { - return false, err - } - - return false, nil -} - -func (h *OpenAccountsIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { - return &lockableAccounts{}, nil -} - -func (h *OpenAccountsIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action, deviceToken *string) error { - typedMetadata := metadata.GetOpenAccounts() - if typedMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - return err - } - - // - // Part 1: Intent ID validation - // - - err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) - if err != nil { - return err - } - - // - // Part 2: Antispam checks against the phone number - // - - if !h.conf.disableAntispamChecks.Get(ctx) { - allow, reason, successCallback, err := h.antispamGuard.AllowOpenAccounts(ctx, initiatiorOwnerAccount, deviceToken) - if err != nil { - return err - } else if !allow { - return newIntentDeniedErrorWithAntispamReason(reason, "antispam guard denied account creation") - } - h.antispamSuccessCallback = successCallback - } - - // - // Part 3: Validate the owner hasn't already created an OpenAccounts intent - // - - _, err = h.data.GetLatestIntentByInitiatorAndType(ctx, intent.OpenAccounts, initiatiorOwnerAccount.PublicKey().ToBase58()) - if err == nil { - return newStaleStateError("already submitted intent to open accounts") - } else if err != intent.ErrIntentNotFound { - return err - } - - // - // Part 4: Validate the individual actions - // - - err = h.validateActions(ctx, initiatiorOwnerAccount, actions) - if err != nil { - return err - } - - // - // Part 5: Local simulation - // - - simResult, err := LocalSimulation(ctx, h.data, actions) - if err != nil { - return err - } - - // - // Part 6: Validate fee payments - // - - return validateFeePayments(ctx, h.data, intentRecord, simResult) -} - -func (h *OpenAccountsIntentHandler) validateActions(ctx context.Context, initiatiorOwnerAccount *common.Account, actions []*transactionpb.Action) error { - expectedActionCount := len(accountTypesToOpen) - if len(actions) != expectedActionCount { - return newIntentValidationErrorf("expected %d total actions", expectedActionCount) - } - - for i, expectedAccountType := range accountTypesToOpen { - openAction := actions[i] - - if openAction.GetOpenAccount() == nil { - return newActionValidationError(openAction, "expected an open account action") - } - - if openAction.GetOpenAccount().AccountType != expectedAccountType { - return newActionValidationErrorf(openAction, "account type must be %s", expectedAccountType) - } - - if openAction.GetOpenAccount().Index != 0 { - return newActionValidationError(openAction, "index must be 0 for all newly opened accounts") - } - - if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatiorOwnerAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "owner must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) - } - - switch expectedAccountType { - case commonpb.AccountType_PRIMARY: - if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(openAction, "authority must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) - } - default: - if bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(openAction, "authority cannot be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) - } - } - - expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) - if err != nil { - return err - } - - if !bytes.Equal(openAction.GetOpenAccount().Token.Value, expectedVaultAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) - } - - if err := validateTimelockUnlockStateDoesntExist(ctx, h.data, openAction.GetOpenAccount()); err != nil { - return err - } - } - - return nil -} - -func (h *OpenAccountsIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { - userAgent, err := client.GetUserAgent(ctx) - if err != nil { - // Should fail much earlier in the account creation flow than here - return err - } - - // Only iOS is eligible for airdrops until we can get stable device IDs - // for Android. - // - // Note: Device attestation guarantees the user agent matches the device - // type that generated the token. - if userAgent.DeviceType != client.DeviceTypeIOS { - err := h.data.MarkIneligibleForAirdrop(ctx, intentRecord.InitiatorOwnerAccount) - if err != nil { - return err - } - } - - eventRecord := &event.Record{ - EventId: intentRecord.IntentId, - EventType: event.AccountCreated, - - SourceCodeAccount: intentRecord.InitiatorOwnerAccount, - - SourceIdentity: *intentRecord.InitiatorPhoneNumber, - - SpamConfidence: 0, - - CreatedAt: time.Now(), - } - event_util.InjectClientDetails(ctx, h.maxmind, eventRecord, true) - - return h.data.SaveEvent(ctx, eventRecord) -} - -func (h *OpenAccountsIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { - if h.antispamSuccessCallback != nil { - // todo: Something more robust, since this is part of the fire & forget - // portion of SubmitIntent - return h.antispamSuccessCallback() - } - - return nil -} - -type SendPrivatePaymentIntentHandler struct { - conf *conf - data code_data.Provider - pusher push_lib.Provider - antispamGuard *antispam.Guard - amlGuard *lawenforcement.AntiMoneyLaunderingGuard - maxmind *maxminddb.Reader - - cachedPaymentRequestRequest *paymentrequest.Record -} - -func NewSendPrivatePaymentIntentHandler( - conf *conf, - data code_data.Provider, - pusher push_lib.Provider, - antispamGuard *antispam.Guard, - amlGuard *lawenforcement.AntiMoneyLaunderingGuard, - maxmind *maxminddb.Reader, -) CreateIntentHandler { - return &SendPrivatePaymentIntentHandler{ - conf: conf, - data: data, - pusher: pusher, - antispamGuard: antispamGuard, - amlGuard: amlGuard, - maxmind: maxmind, - } -} - -func (h *SendPrivatePaymentIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { - typedProtoMetadata := protoMetadata.GetSendPrivatePayment() - if typedProtoMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - exchangeData := typedProtoMetadata.ExchangeData - - // Fetch USD exchange data in a consistent way with the currency server - usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) - if err != nil { - return errors.Wrap(err, "error getting current usd exchange rate") - } - - destination, err := common.NewAccountFromProto(typedProtoMetadata.Destination) - if err != nil { - return err - } - - destinationAccountInfo, err := h.data.GetAccountInfoByTokenAddress(ctx, destination.PublicKey().ToBase58()) - if err != nil && err != account.ErrAccountInfoNotFound { - return err - } - - var isMicroPayment bool - requestRecord, err := h.data.GetRequest(ctx, intentRecord.IntentId) - if err == nil { - if !requestRecord.RequiresPayment() { - return newIntentValidationError("request doesn't require payment") - } - - isMicroPayment = true - h.cachedPaymentRequestRequest = requestRecord - } else if err != paymentrequest.ErrPaymentRequestNotFound { - return err - } - - intentRecord.IntentType = intent.SendPrivatePayment - intentRecord.SendPrivatePaymentMetadata = &intent.SendPrivatePaymentMetadata{ - DestinationTokenAccount: destination.PublicKey().ToBase58(), - Quantity: exchangeData.Quarks, - - ExchangeCurrency: currency_lib.Code(exchangeData.Currency), - ExchangeRate: exchangeData.ExchangeRate, - NativeAmount: typedProtoMetadata.ExchangeData.NativeAmount, - UsdMarketValue: usdExchangeRecord.Rate * float64(kin.FromQuarks(exchangeData.Quarks)), - - IsWithdrawal: typedProtoMetadata.IsWithdrawal, - IsRemoteSend: typedProtoMetadata.IsRemoteSend, - IsMicroPayment: isMicroPayment, - IsTip: typedProtoMetadata.IsTip, - } - - if typedProtoMetadata.IsTip { - if typedProtoMetadata.TippedUser == nil { - return newIntentValidationError("tipped user metadata is missing") - } - - intentRecord.SendPrivatePaymentMetadata.TipMetadata = &intent.TipMetadata{ - Platform: typedProtoMetadata.TippedUser.Platform, - Username: typedProtoMetadata.TippedUser.Username, - } - } - - if destinationAccountInfo != nil { - intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount = destinationAccountInfo.OwnerAccount - } - - return nil -} - -func (h *SendPrivatePaymentIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { - return false, nil -} - -func (h *SendPrivatePaymentIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { - var destinationOwnerAccount, giftCardVaultAccount *common.Account - var err error - - if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) > 0 { - destinationOwnerAccount, err = common.NewAccountFromPublicKeyString(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) - if err != nil { - return nil, err - } - } - - if intentRecord.SendPrivatePaymentMetadata.IsRemoteSend { - giftCardVaultAccount, err = common.NewAccountFromPublicKeyString(intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount) - if err != nil { - return nil, err - } - } - - return &lockableAccounts{ - DestinationOwner: destinationOwnerAccount, - RemoteSendGiftCardVault: giftCardVaultAccount, - }, nil -} - -func (h *SendPrivatePaymentIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action, deviceToken *string) error { - typedMetadata := untypedMetadata.GetSendPrivatePayment() - if typedMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - // todo: need a solution for auto returns - if intentRecord.SendPrivatePaymentMetadata.IsRemoteSend { - return newIntentDeniedError("remote send is not supported yet for the vm") - } - // todo: need a solution for additional memo containing tipping platform and username in a memo - if intentRecord.SendPrivatePaymentMetadata.IsTip { - return newIntentDeniedError("tipping is not supported yet for the vm") - } - - initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - return err - } - - initiatorAccountsByType, err := common.GetLatestCodeTimelockAccountRecordsForOwner(ctx, h.data, initiatiorOwnerAccount) - if err != nil { - return err - } - - initiatorAccounts := make([]*common.AccountRecords, 0) - initiatorAccountsByVault := make(map[string]*common.AccountRecords) - for _, batchRecords := range initiatorAccountsByType { - for _, records := range batchRecords { - initiatorAccounts = append(initiatorAccounts, records) - initiatorAccountsByVault[records.General.TokenAccount] = records - } - } - - // - // Part 1: Antispam and anti-money laundering guard checks against the phone number - // - - if !h.conf.disableAntispamChecks.Get(ctx) { - destination, err := common.NewAccountFromProto(typedMetadata.Destination) - if err != nil { - return err - } - - allow, err := h.antispamGuard.AllowSendPayment(ctx, initiatiorOwnerAccount, false, destination) - if err != nil { - return err - } else if !allow { - return ErrTooManyPayments - } - } - - if !h.conf.disableAmlChecks.Get(ctx) { - allow, err := h.amlGuard.AllowMoneyMovement(ctx, intentRecord) - if err != nil { - return err - } else if !allow { - return ErrTransactionLimitExceeded - } - } - - // - // Part 2: Account validation to determine if it's managed by Code - // - - err = validateAllUserAccountsManagedByCode(ctx, initiatorAccounts) - if err != nil { - return err - } - - // - // Part 3: Exchange data validation - // - - if err := validateExchangeDataWithinIntent(ctx, h.data, intentRecord.IntentId, typedMetadata.ExchangeData); err != nil { - return err - } - - // - // Part 4: Local simulation - // - - simResult, err := LocalSimulation(ctx, h.data, actions) - if err != nil { - return err - } - - // - // Part 5: Validate fee payments - // - - err = validateFeePayments(ctx, h.data, intentRecord, simResult) - if err != nil { - return err - } - - // - // Part 5: Validate the individual actions - // - - return h.validateActions( - ctx, - initiatiorOwnerAccount, - initiatorAccountsByType, - initiatorAccountsByVault, - intentRecord, - typedMetadata, - actions, - simResult, - ) -} - -func (h *SendPrivatePaymentIntentHandler) validateActions( - ctx context.Context, - initiatorOwnerAccount *common.Account, - initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, - initiatorAccountsByVault map[string]*common.AccountRecords, - intentRecord *intent.Record, - metadata *transactionpb.SendPrivatePaymentMetadata, - actions []*transactionpb.Action, - simResult *LocalSimulationResult, -) error { - destination, err := common.NewAccountFromProto(metadata.Destination) - if err != nil { - return err - } - - // - // Part 1: Validate actions match intent metadata - // - - // - // Part 1.1: Check destination and fee collection accounts are paid exact quark amount from latest temp outgoing account - // - - expectedDestinationPayment := int64(metadata.ExchangeData.Quarks) - - // Minimal validation required here since validateFeePayments generically handles - // most metadata that isn't specific to an intent - feePayments := simResult.GetFeePayments() - for _, feePayment := range feePayments { - expectedDestinationPayment += feePayment.DeltaQuarks - } - - destinationSimulation, ok := simResult.SimulationsByAccount[destination.PublicKey().ToBase58()] - if !ok { - return newIntentValidationErrorf("must send payment to destination account %s", destination.PublicKey().ToBase58()) - } else if len(destinationSimulation.Transfers) != 1 { - return newIntentValidationError("destination account can only receive funds in one action") - } else if destinationSimulation.Transfers[0].IsPrivate || !destinationSimulation.Transfers[0].IsWithdraw { - return newActionValidationError(destinationSimulation.Transfers[0].Action, "payment sent to destination must be a public withdraw") - } else if destinationSimulation.GetDeltaQuarks() != expectedDestinationPayment { - return newActionValidationErrorf(destinationSimulation.Transfers[0].Action, "must send %d quarks to destination account", expectedDestinationPayment) - } - - tempOutgoing := initiatorAccountsByType[commonpb.AccountType_TEMPORARY_OUTGOING][0].General.TokenAccount - tempOutgoingSimulation, ok := simResult.SimulationsByAccount[tempOutgoing] - if !ok || len(tempOutgoingSimulation.Transfers) == 0 { - return newIntentValidationErrorf("payment must be sent from temporary outgoing account %s", tempOutgoing) - } - - lastTransferFromTempOutgoing := tempOutgoingSimulation.Transfers[len(tempOutgoingSimulation.Transfers)-1] - if lastTransferFromTempOutgoing.Action.Id != destinationSimulation.Transfers[0].Action.Id { - return newActionValidationErrorf(destinationSimulation.Transfers[0].Action, "destination account must be paid by temporary outgoing account %s", tempOutgoing) - } - - for _, feePayment := range feePayments { - if base58.Encode(feePayment.Action.GetFeePayment().Source.Value) != tempOutgoing { - return newActionValidationErrorf(feePayment.Action, "fee payment must come from temporary outgoing account %s", tempOutgoing) - } - } - - if tempOutgoingSimulation.GetDeltaQuarks() != 0 { - return newIntentValidationErrorf("must fund temporary outgoing account with %d quarks", metadata.ExchangeData.Quarks) - } - - // - // Part 1.2: Check destination account based on withdrawal, remote send and tip flags - // - - // Invalid combination of various payment flags - if metadata.IsRemoteSend && metadata.IsWithdrawal { - return newIntentValidationError("withdrawal and remote send flags cannot both be true set at the same time") - } - if metadata.IsRemoteSend && intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - return newIntentValidationError("remote send cannot be used for micro payments") - } - if metadata.IsRemoteSend && metadata.IsTip { - return newIntentValidationError("remote send cannot be used for tips") - } - if metadata.IsTip && intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - return newIntentValidationError("tips cannot be used for micro payments") - } - if metadata.IsTip && !metadata.IsWithdrawal { - return newIntentValidationError("tips must be a private withdrawal") - } - - // Note: Assumes account info only stores Code user accounts - destinationAccountInfo, err := h.data.GetAccountInfoByTokenAddress(ctx, destination.PublicKey().ToBase58()) - switch err { - case nil: - // The remote send gift card cannot exist. It must be created as part of this intent. - if metadata.IsRemoteSend { - return newIntentValidationError("remote send must be to a brand new gift card account") - } - - if metadata.IsTip && metadata.TippedUser == nil { - return newIntentValidationError("tipped user metadata is missing") - } - if metadata.IsTip && destinationAccountInfo.AccountType != commonpb.AccountType_PRIMARY { - return newIntentValidationError("destination account must be a primary account") - } - if metadata.IsTip { - err = validateTipDestination(ctx, h.data, metadata.TippedUser, destination) - if err != nil { - return err - } - } - - // Code->Code withdrawals must be sent to a deposit account. We allow the - // same owner, since the user might be funding a public withdrawal. - // - // Note: For relationship accounts used in payment requests, the relationship - // has already been validated by the messaging service. - if metadata.IsWithdrawal && destinationAccountInfo.AccountType != commonpb.AccountType_PRIMARY && destinationAccountInfo.AccountType != commonpb.AccountType_RELATIONSHIP { - return newIntentValidationError("destination account must be a deposit account") - } - - if !metadata.IsWithdrawal { - // Code->Code payments must be sent to a temporary incoming account - if destinationAccountInfo.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { - return newIntentValidationError("destination account must be a temporary incoming account") - } - - // That temporary incoming account must be the latest one - latestTempIncomingAccountInfo, err := h.data.GetLatestAccountInfoByOwnerAddressAndType(ctx, destinationAccountInfo.OwnerAccount, commonpb.AccountType_TEMPORARY_INCOMING) - if err != nil { - return err - } else if latestTempIncomingAccountInfo.TokenAccount != destinationAccountInfo.TokenAccount { - // Important Note: Do not leak the account address and break privacy! - return newStaleStateError("destination is not the latest temporary incoming account") - } - - // The temporary incoming account has limited usage - err = validateMinimalTempIncomingAccountUsage(ctx, h.data, destinationAccountInfo) - if err != nil { - return err - } - - // And that payment cannot be done to the same owner - if destinationAccountInfo.OwnerAccount == initiatorOwnerAccount.PublicKey().ToBase58() { - return newIntentValidationError("payments within the same owner are not allowed") - } - } - case account.ErrAccountInfoNotFound: - // There are two cases: - // 1. An external token account that must be validated to exist. - // 2. A remote send gift card that will be created as part of this intent. - if metadata.IsWithdrawal { - if !h.conf.disableBlockchainChecks.Get(ctx) { - err = validateExternalKinTokenAccountWithinIntent(ctx, h.data, destination) - if err != nil { - return err - } - } - } else if metadata.IsRemoteSend { - // No validation needed here. Open validation is handled later. - } else { - // The client is trying a Code->Code payment since withdrawal and remote - // send flags are both false. The temporary incoming account doesn't exist. - return newIntentValidationError("destination account must be a temporary incoming account") - } - default: - return err - } - - // - // Part 1.3: Validate destination account matches payment request, if it exists - // - - if h.cachedPaymentRequestRequest != nil && *h.cachedPaymentRequestRequest.DestinationTokenAccount != destination.PublicKey().ToBase58() { - return newIntentValidationErrorf("payment has a request to destination %s", *h.cachedPaymentRequestRequest.DestinationTokenAccount) - } - - // - // Part 2: Validate actions that open/close accounts - // - - openedAccounts := simResult.GetOpenedAccounts() - if metadata.IsRemoteSend { - // There are two opened accounts, one for the gift card and one for the temporary outgoing account - if len(openedAccounts) != 2 { - return newIntentValidationError("must open two accounts") - } - - err = validateGiftCardAccountOpened( - ctx, - h.data, - initiatorOwnerAccount, - initiatorAccountsByType, - destination, - actions, - ) - if err != nil { - return err - } - } else { - // There's one opened account, and it must be a new temporary outgoing account. - if len(openedAccounts) != 1 { - return newIntentValidationError("must open one account") - } - } - - err = validateNextTemporaryAccountOpened( - commonpb.AccountType_TEMPORARY_OUTGOING, - initiatorOwnerAccount, - initiatorAccountsByType, - actions, - ) - if err != nil { - return err - } - - // There's one closed account, and it must be the latest temporary outgoing account. - closedAccounts := simResult.GetClosedAccounts() - if len(closedAccounts) != 1 { - return newIntentValidationError("must close one account") - } else if closedAccounts[0].TokenAccount.PublicKey().ToBase58() != tempOutgoing { - return newActionValidationError(closedAccounts[0].CloseAction, "must close latest temporary outgoing account") - } - - // - // Part 3: Validate actions that move money - // - - err = validateMoneyMovementActionCount(actions) - if err != nil { - return err - } - - for _, simulation := range simResult.SimulationsByAccount { - if len(simulation.Transfers) == 0 { - continue - } - - // Destination account transfers are already validated during intent - // metadata validation - if simulation.TokenAccount.PublicKey().ToBase58() == destination.PublicKey().ToBase58() { - continue - } - - // By previous validation, we know the only opened account is the new temp - // outgoing account - if simulation.Opened { - return newActionValidationError(simulation.Transfers[0].Action, "new temporary outgoing account cannot send/receive kin") - } - - // Every account going forward must be part of the initiator owner's latest - // account set. - accountRecords, ok := initiatorAccountsByVault[simulation.TokenAccount.PublicKey().ToBase58()] - if !ok { - usedAs := "source" - if simulation.Transfers[0].DeltaQuarks > 0 { - usedAs = "destination" - } - return newActionValidationErrorf(simulation.Transfers[0].Action, "%s is not a latest owned account", usedAs) - } - - // Enforce how each account can send/receive Kin - switch accountRecords.General.AccountType { - case commonpb.AccountType_PRIMARY: - return newActionValidationError(simulation.Transfers[0].Action, "primary account cannot send/receive kin") - case commonpb.AccountType_TEMPORARY_INCOMING: - return newActionValidationError(simulation.Transfers[0].Action, "temporary incoming account cannot send/receive kin") - case commonpb.AccountType_TEMPORARY_OUTGOING: - expectedPublicTransfers := 1 - if intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - // Code fee, plus any additional configured fee takers - expectedPublicTransfers += len(h.cachedPaymentRequestRequest.Fees) + 1 - } - if simulation.CountPublicTransfers() != expectedPublicTransfers { - return newIntentValidationErrorf("temporary outgoing account can only have %d public transfers", expectedPublicTransfers) - } - - if simulation.CountWithdrawals() != 1 { - return newIntentValidationError("temporary outgoing account can only have one public withdraw") - } - - if simulation.CountOutgoingTransfers() != expectedPublicTransfers { - return newIntentValidationErrorf("temporary outgoing account can only send kin %d times", expectedPublicTransfers) - } - case commonpb.AccountType_BUCKET_1_KIN, - commonpb.AccountType_BUCKET_10_KIN, - commonpb.AccountType_BUCKET_100_KIN, - commonpb.AccountType_BUCKET_1_000_KIN, - commonpb.AccountType_BUCKET_10_000_KIN, - commonpb.AccountType_BUCKET_100_000_KIN, - commonpb.AccountType_BUCKET_1_000_000_KIN: - - err = validateBucketAccountSimulation(accountRecords.General.AccountType, simulation) - if err != nil { - return err - } - default: - return errors.New("unhandled account type") - } - } - - err = validateMoneyMovementActionUserAccounts(intent.SendPrivatePayment, initiatorAccountsByVault, actions) - if err != nil { - return err - } - - // - // Part 4: Validate there are no update actions - // - - return validateNoUpgradeActions(actions) -} - -func (h *SendPrivatePaymentIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { - var eventRecord *event.Record - if !intentRecord.SendPrivatePaymentMetadata.IsRemoteSend && !intentRecord.SendPrivatePaymentMetadata.IsWithdrawal && !intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - // todo: Collect additional destination info on private receive - eventRecord = &event.Record{ - EventId: intentRecord.IntentId, - EventType: event.InPersonGrab, - - SourceCodeAccount: intentRecord.InitiatorOwnerAccount, - DestinationCodeAccount: &intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount, - - SourceIdentity: *intentRecord.InitiatorPhoneNumber, - - UsdValue: &intentRecord.SendPrivatePaymentMetadata.UsdMarketValue, - - SpamConfidence: 0, - - CreatedAt: time.Now(), - } - } else if intentRecord.SendPrivatePaymentMetadata.IsRemoteSend { - eventRecord = &event.Record{ - EventId: intentRecord.IntentId, - EventType: event.RemoteSend, - - SourceCodeAccount: intentRecord.InitiatorOwnerAccount, - - SourceIdentity: *intentRecord.InitiatorPhoneNumber, - - UsdValue: &intentRecord.SendPrivatePaymentMetadata.UsdMarketValue, - - SpamConfidence: 0, - - CreatedAt: time.Now(), - } - } else if intentRecord.SendPrivatePaymentMetadata.IsMicroPayment { - eventRecord = &event.Record{ - EventId: intentRecord.IntentId, - EventType: event.MicroPayment, - - SourceCodeAccount: intentRecord.InitiatorOwnerAccount, - - SourceIdentity: *intentRecord.InitiatorPhoneNumber, - - UsdValue: &intentRecord.SendPrivatePaymentMetadata.UsdMarketValue, - - SpamConfidence: 0, - - CreatedAt: time.Now(), - } - - if len(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) > 0 { - eventRecord.DestinationCodeAccount = &intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount - } else { - eventRecord.ExternalTokenAccount = &intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount - } - } - - if eventRecord != nil { - event_util.InjectClientDetails(ctx, h.maxmind, eventRecord, true) - - if eventRecord.DestinationCodeAccount != nil { - destinationVerificationRecord, err := h.data.GetLatestPhoneVerificationForAccount(ctx, *eventRecord.DestinationCodeAccount) - if err != nil { - return err - } - eventRecord.DestinationIdentity = &destinationVerificationRecord.PhoneNumber - } - - err := h.data.SaveEvent(ctx, eventRecord) - if err != nil { - return err - } - } - - return nil -} - -func (h *SendPrivatePaymentIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} - -type ReceivePaymentsPrivatelyIntentHandler struct { - conf *conf - data code_data.Provider - antispamGuard *antispam.Guard - amlGuard *lawenforcement.AntiMoneyLaunderingGuard -} - -func NewReceivePaymentsPrivatelyIntentHandler(conf *conf, data code_data.Provider, antispamGuard *antispam.Guard, amlGuard *lawenforcement.AntiMoneyLaunderingGuard) CreateIntentHandler { - return &ReceivePaymentsPrivatelyIntentHandler{ - conf: conf, - data: data, - antispamGuard: antispamGuard, - amlGuard: amlGuard, - } -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { - typedProtoMetadata := protoMetadata.GetReceivePaymentsPrivately() - if typedProtoMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) - if err != nil { - return errors.Wrap(err, "error getting current usd exchange rate") - } - - intentRecord.IntentType = intent.ReceivePaymentsPrivately - intentRecord.ReceivePaymentsPrivatelyMetadata = &intent.ReceivePaymentsPrivatelyMetadata{ - Source: base58.Encode(typedProtoMetadata.Source.Value), - Quantity: typedProtoMetadata.Quarks, - IsDeposit: typedProtoMetadata.IsDeposit, - - UsdMarketValue: usdExchangeRecord.Rate * float64(kin.FromQuarks(typedProtoMetadata.Quarks)), - } - - return nil -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { - return false, nil -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { - return &lockableAccounts{}, nil -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action, deviceToken *string) error { - typedMetadata := untypedMetadata.GetReceivePaymentsPrivately() - if typedMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - return err - } - - initiatorAccountsByType, err := common.GetLatestCodeTimelockAccountRecordsForOwner(ctx, h.data, initiatiorOwnerAccount) - if err != nil { - return err - } - - initiatorAccounts := make([]*common.AccountRecords, 0) - initiatorAccountsByVault := make(map[string]*common.AccountRecords) - for _, batchRecords := range initiatorAccountsByType { - for _, records := range batchRecords { - initiatorAccounts = append(initiatorAccounts, records) - initiatorAccountsByVault[records.General.TokenAccount] = records - } - } - - // - // Part 1: Intent ID validation - // - - err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) - if err != nil { - return err - } - - // - // Part 2: Antispam and anti-money laundering guard checks against the phone number - // - if !h.conf.disableAntispamChecks.Get(ctx) { - allow, err := h.antispamGuard.AllowReceivePayments(ctx, initiatiorOwnerAccount, false) - if err != nil { - return err - } else if !allow { - return ErrTooManyPayments - } - } - - if !h.conf.disableAmlChecks.Get(ctx) { - allow, err := h.amlGuard.AllowMoneyMovement(ctx, intentRecord) - if err != nil { - return err - } else if !allow { - return ErrTransactionLimitExceeded - } - } - - // - // Part 3: Account validation to determine if it's managed by Code - // - - err = validateAllUserAccountsManagedByCode(ctx, initiatorAccounts) - if err != nil { - return err - } - - // - // Part 4: Local simulation - // - - simResult, err := LocalSimulation(ctx, h.data, actions) - if err != nil { - return err - } - - // - // Part 5: Validate fee payments - // - - err = validateFeePayments(ctx, h.data, intentRecord, simResult) - if err != nil { - return err - } - - // - // Part 6: Validate the individual actions - // - - return h.validateActions( - ctx, - initiatiorOwnerAccount, - initiatorAccountsByType, - initiatorAccountsByVault, - typedMetadata, - actions, - simResult, - ) -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) validateActions( - ctx context.Context, - initiatorOwnerAccount *common.Account, - initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, - initiatorAccountsByVault map[string]*common.AccountRecords, - metadata *transactionpb.ReceivePaymentsPrivatelyMetadata, - actions []*transactionpb.Action, - simResult *LocalSimulationResult, -) error { - source, err := common.NewAccountFromProto(metadata.Source) - if err != nil { - return err - } - - // - // Part 1: Validate actions match intent metadata - // - - // - // Part 1.1: Check exact quark amount is received from the source account - // - - sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] - if !ok || len(sourceSimulation.Transfers) == 0 { - return newIntentValidationError("must receive payments from source account") - } else if sourceSimulation.GetDeltaQuarks() != -int64(metadata.Quarks) { - return newIntentValidationErrorf("must receive %d quarks from source account", metadata.Quarks) - } - - // - // Part 1.2: Check source account type based on deposit flag - // - - sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] - if !ok { - return newIntentValidationError("source is not a latest owned account") - } else if metadata.IsDeposit && sourceAccountInfo.General.AccountType != commonpb.AccountType_PRIMARY && sourceAccountInfo.General.AccountType != commonpb.AccountType_RELATIONSHIP { - return newIntentValidationError("must receive from a deposit account") - } else if !metadata.IsDeposit && sourceAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { - return newIntentValidationError("must receive from latest temporary incoming account") - } - - // - // Part 2: Validate actions that open/close accounts - // - - openedAccounts := simResult.GetOpenedAccounts() - closedAccounts := simResult.GetClosedAccounts() - if metadata.IsDeposit { - if len(openedAccounts) > 0 { - return newActionValidationError(openedAccounts[0].OpenAction, "cannot open any account") - } - - if len(closedAccounts) > 0 { - return newActionValidationError(closedAccounts[0].CloseAction, "cannot close any account") - } - } else { - - // There's one opened account, and it must be the new temporary incoming account. - if len(openedAccounts) != 1 { - return newIntentValidationError("must open one account") - } - - err = validateNextTemporaryAccountOpened( - commonpb.AccountType_TEMPORARY_INCOMING, - initiatorOwnerAccount, - initiatorAccountsByType, - actions, - ) - if err != nil { - return err - } - - // No accounts are closed, the latest temporary incoming account is simply - // compressed after used. - closedAccounts := simResult.GetClosedAccounts() - if len(closedAccounts) != 0 { - return newIntentValidationError("cannot close any account") - } - } - - // - // Part 3: Validate actions that move money - // - - err = validateMoneyMovementActionCount(actions) - if err != nil { - return err - } - - for _, simulation := range simResult.SimulationsByAccount { - if len(simulation.Transfers) == 0 { - continue - } - - // By previous validation, we know the only opened account is the new temp - // incoming account - if simulation.Opened { - return newActionValidationError(simulation.Transfers[0].Action, "new temporary incoming account cannot send/receive kin") - } - - // Every account going forward must be part of the initiator owner's latest - // account set. - accountRecords, ok := initiatorAccountsByVault[simulation.TokenAccount.PublicKey().ToBase58()] - if !ok { - usedAs := "source" - if simulation.Transfers[0].DeltaQuarks > 0 { - usedAs = "destination" - } - return newActionValidationErrorf(simulation.Transfers[0].Action, "%s is not a latest owned account", usedAs) - } - - // Enforce how each account can send/receive Kin - switch accountRecords.General.AccountType { - case commonpb.AccountType_TEMPORARY_OUTGOING: - return newActionValidationError(simulation.Transfers[0].Action, "temporary outgoing account cannot send/receive kin") - case commonpb.AccountType_PRIMARY, commonpb.AccountType_RELATIONSHIP: - if !metadata.IsDeposit { - return newActionValidationError(simulation.Transfers[0].Action, "deposit account cannot send/receive kin") - } - - if metadata.IsDeposit { - incomingTransfers := simulation.GetIncomingTransfers() - if len(incomingTransfers) > 0 { - return newActionValidationError(incomingTransfers[0].Action, "deposit account cannot receive kin") - } - - publicTransfers := simulation.GetPublicTransfers() - if len(publicTransfers) > 0 { - return newActionValidationError(publicTransfers[0].Action, "receipt of kin must be private") - } - - withdraws := simulation.GetWithdraws() - if len(withdraws) > 0 { - return newActionValidationError(withdraws[0].Action, "receipt of kin cannot be done with a withdraw") - } - } - case commonpb.AccountType_TEMPORARY_INCOMING: - if metadata.IsDeposit { - return newActionValidationError(simulation.Transfers[0].Action, "temporary incoming account cannot send/receive kin") - } - - if !metadata.IsDeposit { - incomingTransfers := simulation.GetIncomingTransfers() - if len(incomingTransfers) > 0 { - return newActionValidationError(incomingTransfers[0].Action, "temporary incoming account cannot receive kin") - } - - publicTransfers := simulation.GetPublicTransfers() - if len(publicTransfers) > 0 { - return newActionValidationError(publicTransfers[0].Action, "receipt of kin must be private") - } - - withdraws := simulation.GetWithdraws() - if len(withdraws) > 0 { - return newActionValidationError(withdraws[0].Action, "receipt of kin cannot be done with a withdraw") - } - } - case commonpb.AccountType_BUCKET_1_KIN, - commonpb.AccountType_BUCKET_10_KIN, - commonpb.AccountType_BUCKET_100_KIN, - commonpb.AccountType_BUCKET_1_000_KIN, - commonpb.AccountType_BUCKET_10_000_KIN, - commonpb.AccountType_BUCKET_100_000_KIN, - commonpb.AccountType_BUCKET_1_000_000_KIN: - - err = validateBucketAccountSimulation(accountRecords.General.AccountType, simulation) - if err != nil { - return err - } - default: - return errors.New("unhandled account type") - } - } - - err = validateMoneyMovementActionUserAccounts(intent.ReceivePaymentsPrivately, initiatorAccountsByVault, actions) - if err != nil { - return err - } - - // - // Part 4: Validate there are no upgrade actions - // - - return validateNoUpgradeActions(actions) -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} - -func (h *ReceivePaymentsPrivatelyIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} - -type UpgradePrivacyIntentHandler struct { - conf *conf - data code_data.Provider - cachedUpgradeTargets map[uint32]*privacyUpgradeCandidate -} - -func NewUpgradePrivacyIntentHandler(conf *conf, data code_data.Provider) UpdateIntentHandler { - return &UpgradePrivacyIntentHandler{ - conf: conf, - data: data, - } -} - -func (h *UpgradePrivacyIntentHandler) AllowUpdate(ctx context.Context, existingIntent *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action) error { - if untypedMetadata.GetUpgradePrivacy() == nil { - return errors.New("unexpected metadata proto message") - } - - cachedUpgradeTargets := make(map[uint32]*privacyUpgradeCandidate) - for _, untypedAction := range actions { - var actionId uint32 - switch typedAction := untypedAction.Type.(type) { - case *transactionpb.Action_PermanentPrivacyUpgrade: - actionId = typedAction.PermanentPrivacyUpgrade.ActionId - _, ok := cachedUpgradeTargets[actionId] - if ok { - return newActionValidationError(untypedAction, "duplicate upgrade action detected") - } - default: - return newActionValidationError(untypedAction, "all actions must be to upgrade private transactions") - } - - cachedUpgradeTarget, err := selectCandidateForPrivacyUpgrade(ctx, h.data, existingIntent.IntentId, actionId) - switch err { - case nil: - case ErrPrivacyAlreadyUpgraded: - return newActionWithStaleStateError(untypedAction, err.Error()) - case ErrInvalidActionToUpgrade, ErrPrivacyUpgradeMissed, ErrWaitForNextBlock: - return newActionValidationError(untypedAction, err.Error()) - default: - return err - } - cachedUpgradeTargets[actionId] = cachedUpgradeTarget - } - h.cachedUpgradeTargets = cachedUpgradeTargets - - return nil -} - -func (h *UpgradePrivacyIntentHandler) GetCachedUpgradeTarget(protoAction *transactionpb.PermanentPrivacyUpgradeAction) (*privacyUpgradeCandidate, bool) { - upgradeTo, ok := h.cachedUpgradeTargets[protoAction.ActionId] - return upgradeTo, ok -} - -type SendPublicPaymentIntentHandler struct { - conf *conf - data code_data.Provider - pusher push_lib.Provider - antispamGuard *antispam.Guard - maxmind *maxminddb.Reader -} - -func NewSendPublicPaymentIntentHandler( - conf *conf, - data code_data.Provider, - pusher push_lib.Provider, - antispamGuard *antispam.Guard, - maxmind *maxminddb.Reader, -) CreateIntentHandler { - return &SendPublicPaymentIntentHandler{ - conf: conf, - data: data, - pusher: pusher, - antispamGuard: antispamGuard, - maxmind: maxmind, - } -} - -func (h *SendPublicPaymentIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { - typedProtoMetadata := protoMetadata.GetSendPublicPayment() - if typedProtoMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - exchangeData := typedProtoMetadata.ExchangeData - - usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) - if err != nil { - return errors.Wrap(err, "error getting current usd exchange rate") - } - - destination, err := common.NewAccountFromProto(typedProtoMetadata.Destination) - if err != nil { - return err - } - - destinationAccountInfo, err := h.data.GetAccountInfoByTokenAddress(ctx, destination.PublicKey().ToBase58()) - if err != nil && err != account.ErrAccountInfoNotFound { - return err - } - - intentRecord.IntentType = intent.SendPublicPayment - intentRecord.SendPublicPaymentMetadata = &intent.SendPublicPaymentMetadata{ - DestinationTokenAccount: destination.PublicKey().ToBase58(), - Quantity: exchangeData.Quarks, - - ExchangeCurrency: currency_lib.Code(exchangeData.Currency), - ExchangeRate: exchangeData.ExchangeRate, - NativeAmount: typedProtoMetadata.ExchangeData.NativeAmount, - UsdMarketValue: usdExchangeRecord.Rate * float64(kin.FromQuarks(exchangeData.Quarks)), - - IsWithdrawal: typedProtoMetadata.IsWithdrawal, - } - - if destinationAccountInfo != nil { - intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount = destinationAccountInfo.OwnerAccount - } - - return nil -} - -func (h *SendPublicPaymentIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { - return false, nil -} - -func (h *SendPublicPaymentIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { - if len(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) == 0 { - return &lockableAccounts{}, nil - } - - destinationOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) - if err != nil { - return nil, err - } - - return &lockableAccounts{ - DestinationOwner: destinationOwnerAccount, - }, nil -} - -func (h *SendPublicPaymentIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action, deviceToken *string) error { - typedMetadata := untypedMetadata.GetSendPublicPayment() - if typedMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - return err - } - - initiatorAccountsByType, err := common.GetLatestCodeTimelockAccountRecordsForOwner(ctx, h.data, initiatiorOwnerAccount) - if err != nil { - return err - } - - initiatorAccounts := make([]*common.AccountRecords, 0) - initiatorAccountsByVault := make(map[string]*common.AccountRecords) - for _, batchRecords := range initiatorAccountsByType { - for _, records := range batchRecords { - initiatorAccounts = append(initiatorAccounts, records) - initiatorAccountsByVault[records.General.TokenAccount] = records - } - } - - // - // Part 1: Intent ID validation - // - - err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) - if err != nil { - return err - } - - // - // Part 2: Antispam guard checks against the phone number - // - - if !h.conf.disableAntispamChecks.Get(ctx) { - destination, err := common.NewAccountFromProto(typedMetadata.Destination) - if err != nil { - return err - } - - allow, err := h.antispamGuard.AllowSendPayment(ctx, initiatiorOwnerAccount, true, destination) - if err != nil { - return err - } else if !allow { - return ErrTooManyPayments - } - } - - // - // Part 3: Account validation to determine if it's managed by Code - // - - err = validateAllUserAccountsManagedByCode(ctx, initiatorAccounts) - if err != nil { - return err - } - - // - // Part 4: Exchange data validation - // - - if err := validateExchangeDataWithinIntent(ctx, h.data, intentRecord.IntentId, typedMetadata.ExchangeData); err != nil { - return err - } - - // - // Part 5: Local simulation - // - - simResult, err := LocalSimulation(ctx, h.data, actions) - if err != nil { - return err - } - - // - // Part 6: Validate fee payments - // - - err = validateFeePayments(ctx, h.data, intentRecord, simResult) - if err != nil { - return err - } - - // - // Part 7: Validate the individual actions - // - - return h.validateActions( - ctx, - initiatiorOwnerAccount, - initiatorAccountsByType, - initiatorAccountsByVault, - intentRecord, - typedMetadata, - actions, - simResult, - ) -} - -func (h *SendPublicPaymentIntentHandler) validateActions( - ctx context.Context, - initiatorOwnerAccount *common.Account, - initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, - initiatorAccountsByVault map[string]*common.AccountRecords, - intentRecord *intent.Record, - metadata *transactionpb.SendPublicPaymentMetadata, - actions []*transactionpb.Action, - simResult *LocalSimulationResult, -) error { - if len(actions) != 1 { - return newIntentValidationError("expected 1 action") - } - - var source *common.Account - var err error - if metadata.Source != nil { - source, err = common.NewAccountFromProto(metadata.Source) - if err != nil { - return err - } - } else { - // Backwards compat for old clients using metadata without source. It was - // always assumed to be from the primary account - source, err = common.NewAccountFromPublicKeyString(initiatorAccountsByType[commonpb.AccountType_PRIMARY][0].General.TokenAccount) - if err != nil { - return err - } - } - - destination, err := common.NewAccountFromProto(metadata.Destination) - if err != nil { - return err - } - - // Part 1: Check the source and destination accounts are valid - - destinationAccountInfo, err := h.data.GetAccountInfoByTokenAddress(ctx, destination.PublicKey().ToBase58()) - switch err { - case nil: - // Code->Code public withdraws must be done against other deposit accounts - if metadata.IsWithdrawal && destinationAccountInfo.AccountType != commonpb.AccountType_PRIMARY && destinationAccountInfo.AccountType != commonpb.AccountType_RELATIONSHIP { - return newIntentValidationError("destination account must be a deposit account") - } - - // And the destination cannot be the source of funds, since that results in a no-op - if source.PublicKey().ToBase58() == destinationAccountInfo.TokenAccount { - return newIntentValidationError("payment is a no-op") - } - case account.ErrAccountInfoNotFound: - // Check whether the destination account is a Kin token account that's - // been created on the blockchain. - if !h.conf.disableBlockchainChecks.Get(ctx) { - err = validateExternalKinTokenAccountWithinIntent(ctx, h.data, destination) - if err != nil { - return err - } - } - default: - return err - } - - sourceAccountRecords, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] - if !ok || (sourceAccountRecords.General.AccountType != commonpb.AccountType_PRIMARY && sourceAccountRecords.General.AccountType != commonpb.AccountType_RELATIONSHIP) { - return newIntentValidationError("source account must be a deposit account") - } - - // - // Part 2: Validate actions match intent metadata - // - - // - // Part 2.1: Check destination account is paid exact quark amount from the deposit account - // - - destinationSimulation, ok := simResult.SimulationsByAccount[destination.PublicKey().ToBase58()] - if !ok { - return newIntentValidationErrorf("must send payment to destination account %s", destination.PublicKey().ToBase58()) - } else if destinationSimulation.Transfers[0].IsPrivate || destinationSimulation.Transfers[0].IsWithdraw { - return newActionValidationError(destinationSimulation.Transfers[0].Action, "payment sent to destination must be a public transfer") - } else if destinationSimulation.GetDeltaQuarks() != int64(metadata.ExchangeData.Quarks) { - return newActionValidationErrorf(destinationSimulation.Transfers[0].Action, "must send %d quarks to destination account", metadata.ExchangeData.Quarks) - } - - // - // Part 2.2: Check that the user's deposit account was used as the source of funds - // as specified in the metadata - // - - sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] - if !ok { - return newIntentValidationErrorf("must send payment from source account %s", source.PublicKey().ToBase58()) - } else if sourceSimulation.GetDeltaQuarks() != -int64(metadata.ExchangeData.Quarks) { - return newActionValidationErrorf(sourceSimulation.Transfers[0].Action, "must send %d quarks from source account", metadata.ExchangeData.Quarks) - } - - // Part 3: Generic validation of actions that move money - - err = validateMoneyMovementActionUserAccounts(intent.SendPublicPayment, initiatorAccountsByVault, actions) - if err != nil { - return err - } - - // Part 4: Sanity check no open and closed accounts - - if len(simResult.GetOpenedAccounts()) > 0 { - return newIntentValidationError("cannot open any account") - } - - if len(simResult.GetClosedAccounts()) > 0 { - return newIntentValidationError("cannot close any account") - } - - return nil -} - -func (h *SendPublicPaymentIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { - if intentRecord.SendPublicPaymentMetadata.IsWithdrawal && len(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) == 0 { - eventRecord := &event.Record{ - EventId: intentRecord.IntentId, - EventType: event.Withdrawal, - - SourceCodeAccount: intentRecord.InitiatorOwnerAccount, - ExternalTokenAccount: &intentRecord.SendPublicPaymentMetadata.DestinationTokenAccount, - - SourceIdentity: *intentRecord.InitiatorPhoneNumber, - - UsdValue: &intentRecord.SendPublicPaymentMetadata.UsdMarketValue, - - SpamConfidence: 0, - - CreatedAt: time.Now(), - } - event_util.InjectClientDetails(ctx, h.maxmind, eventRecord, true) - - err := h.data.SaveEvent(ctx, eventRecord) - if err != nil { - return err - } - } - - return nil -} - -func (h *SendPublicPaymentIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} - -type ReceivePaymentsPubliclyIntentHandler struct { - conf *conf - data code_data.Provider - antispamGuard *antispam.Guard - maxmind *maxminddb.Reader - - cachedGiftCardIssuedIntentRecord *intent.Record -} - -func NewReceivePaymentsPubliclyIntentHandler(conf *conf, data code_data.Provider, antispamGuard *antispam.Guard, maxmind *maxminddb.Reader) CreateIntentHandler { - return &ReceivePaymentsPubliclyIntentHandler{ - conf: conf, - data: data, - antispamGuard: antispamGuard, - maxmind: maxmind, - } -} - -func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { - typedProtoMetadata := protoMetadata.GetReceivePaymentsPublicly() - if typedProtoMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - giftCardVault, err := common.NewAccountFromPublicKeyBytes(typedProtoMetadata.Source.Value) - if err != nil { - return err - } - - usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) - if err != nil { - return errors.Wrap(err, "error getting current usd exchange rate") - } - - // This is an optimization for payment history. Original fiat amounts are not - // easily linked due to the nature of gift cards and the remote send flow. We - // fetch this metadata up front so we don't need to do it every time in history. - giftCardIssuedIntentRecord, err := h.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVault.PublicKey().ToBase58()) - if err == intent.ErrIntentNotFound { - return newIntentValidationError("source is not a remote send gift card") - } else if err != nil { - return err - } - h.cachedGiftCardIssuedIntentRecord = giftCardIssuedIntentRecord - - intentRecord.IntentType = intent.ReceivePaymentsPublicly - intentRecord.ReceivePaymentsPubliclyMetadata = &intent.ReceivePaymentsPubliclyMetadata{ - Source: giftCardVault.PublicKey().ToBase58(), - Quantity: typedProtoMetadata.Quarks, - IsRemoteSend: typedProtoMetadata.IsRemoteSend, - IsReturned: false, - IsIssuerVoidingGiftCard: typedProtoMetadata.IsIssuerVoidingGiftCard, - - OriginalExchangeCurrency: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeCurrency, - OriginalExchangeRate: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeRate, - OriginalNativeAmount: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.NativeAmount, - - UsdMarketValue: usdExchangeRecord.Rate * float64(kin.FromQuarks(typedProtoMetadata.Quarks)), - } - - if intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard && intentRecord.InitiatorOwnerAccount != giftCardIssuedIntentRecord.InitiatorOwnerAccount { - return newIntentValidationError("only the issuer can void the gift card") - } - - return nil -} - -func (h *ReceivePaymentsPubliclyIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { - return false, nil -} - -func (h *ReceivePaymentsPubliclyIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { - if !intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend { - return &lockableAccounts{}, nil - } - - giftCardVaultAccount, err := common.NewAccountFromPublicKeyString(intentRecord.ReceivePaymentsPubliclyMetadata.Source) - if err != nil { - return nil, err - } - - return &lockableAccounts{ - RemoteSendGiftCardVault: giftCardVaultAccount, - }, nil -} - -func (h *ReceivePaymentsPubliclyIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action, deviceToken *string) error { - typedMetadata := untypedMetadata.GetReceivePaymentsPublicly() - if typedMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - if !typedMetadata.IsRemoteSend { - return newIntentValidationError("only remote send is supported") - } - - if typedMetadata.ExchangeData != nil { - return newIntentValidationError("exchange data cannot be set") - } - - initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - return err - } - - giftCardVaultAccount, err := common.NewAccountFromPublicKeyString(intentRecord.ReceivePaymentsPubliclyMetadata.Source) - if err != nil { - return err - } - - initiatorAccountsByType, err := common.GetLatestCodeTimelockAccountRecordsForOwner(ctx, h.data, initiatiorOwnerAccount) - if err != nil { - return err - } - - initiatorAccounts := make([]*common.AccountRecords, 0) - initiatorAccountsByVault := make(map[string]*common.AccountRecords) - for _, batchRecords := range initiatorAccountsByType { - for _, records := range batchRecords { - initiatorAccounts = append(initiatorAccounts, records) - initiatorAccountsByVault[records.General.TokenAccount] = records - } - } - - // - // Part 1: Intent ID validation - // - - err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) - if err != nil { - return err - } - - // - // Part 2: Antispam guard checks against the phone number - // - if !h.conf.disableAntispamChecks.Get(ctx) { - allow, err := h.antispamGuard.AllowReceivePayments(ctx, initiatiorOwnerAccount, true) - if err != nil { - return err - } else if !allow { - return ErrTooManyPayments - } - } - - // - // Part 3: User account validation to determine if it's managed by Code - // - - err = validateAllUserAccountsManagedByCode(ctx, initiatorAccounts) - if err != nil { - return err - } - - // - // Part 4: Gift card account validation - // - - err = validateClaimedGiftCard(ctx, h.data, giftCardVaultAccount, typedMetadata.Quarks) - if err != nil { - return err - } - - // - // Part 5: Local simulation - // - - simResult, err := LocalSimulation(ctx, h.data, actions) - if err != nil { - return err - } - - // - // Part 6: Validate fee payments - // - - err = validateFeePayments(ctx, h.data, intentRecord, simResult) - if err != nil { - return err - } - - // - // Part 7: Validate the individual actions - // - - return h.validateActions( - ctx, - initiatiorOwnerAccount, - initiatorAccountsByType, - initiatorAccountsByVault, - typedMetadata, - actions, - simResult, - ) -} - -func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( - ctx context.Context, - initiatorOwnerAccount *common.Account, - initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, - initiatorAccountsByVault map[string]*common.AccountRecords, - metadata *transactionpb.ReceivePaymentsPubliclyMetadata, - actions []*transactionpb.Action, - simResult *LocalSimulationResult, -) error { - if len(actions) != 1 { - return newIntentValidationError("expected 1 action") - } - - // - // Part 1: Validate source and destination accounts are valid to use - // - - // Note: Already validated to be a claimable gift card elsewhere - source, err := common.NewAccountFromProto(metadata.Source) - if err != nil { - return err - } - - // The destination account must be the latest temporary incoming account - destinationAccountInfo := initiatorAccountsByType[commonpb.AccountType_TEMPORARY_INCOMING][0].General - destinationSimulation, ok := simResult.SimulationsByAccount[destinationAccountInfo.TokenAccount] - if !ok { - return newActionValidationError(actions[0], "must send payment to latest temp incoming account") - } - - // And that temporary incoming account has limited usage - err = validateMinimalTempIncomingAccountUsage(ctx, h.data, destinationAccountInfo) - if err != nil { - return err - } - - // - // Part 2: Validate actions match intent - // - - // - // Part 2.1: Check source account pays exact quark amount to destination in a public withdraw - // - - sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] - if !ok { - return newIntentValidationError("must receive payments from source account") - } else if sourceSimulation.GetDeltaQuarks() != -int64(metadata.Quarks) { - return newActionValidationErrorf(sourceSimulation.Transfers[0].Action, "must receive %d quarks from source account", metadata.Quarks) - } else if sourceSimulation.Transfers[0].IsPrivate || !sourceSimulation.Transfers[0].IsWithdraw { - return newActionValidationError(sourceSimulation.Transfers[0].Action, "transfer must be a public withdraw") - } - - // - // Part 2.2: Check destination account is paid exact quark amount from source account in a public withdraw - // - - if destinationSimulation.GetDeltaQuarks() != int64(metadata.Quarks) { - return newActionValidationErrorf(actions[0], "must receive %d quarks to temp incoming account", metadata.Quarks) - } else if destinationSimulation.Transfers[0].IsPrivate || !destinationSimulation.Transfers[0].IsWithdraw { - return newActionValidationError(sourceSimulation.Transfers[0].Action, "transfer must be a public withdraw") - } - - // - // Part 3: Validate accounts that are opened and closed - // - - if len(simResult.GetOpenedAccounts()) > 0 { - return newIntentValidationError("cannot open any account") - } - - closedAccounts := simResult.GetClosedAccounts() - if len(closedAccounts) != 1 { - return newIntentValidationError("must close 1 account") - } else if closedAccounts[0].TokenAccount.PublicKey().ToBase58() != source.PublicKey().ToBase58() { - return newActionValidationError(actions[0], "must close source account") - } - - // - // Part 4: Generic validation of actions that move money - // - - return validateMoneyMovementActionUserAccounts(intent.ReceivePaymentsPublicly, initiatorAccountsByVault, actions) -} - -func (h *ReceivePaymentsPubliclyIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { - if intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend { - eventRecord, err := h.data.GetEvent(ctx, h.cachedGiftCardIssuedIntentRecord.IntentId) - if err == nil { - eventRecord.DestinationCodeAccount = &intentRecord.InitiatorOwnerAccount - eventRecord.DestinationIdentity = pointer.StringCopy(intentRecord.InitiatorPhoneNumber) - event_util.InjectClientDetails(ctx, h.maxmind, eventRecord, false) // Will be AWS if desktop - - err = h.data.SaveEvent(ctx, eventRecord) - if err != nil { - return err - } - } else if err != event.ErrEventNotFound { // todo: can be dropped when older gift cards (prior to tracking) have been resolved prior - return err - } - } - - return nil -} - -func (h *ReceivePaymentsPubliclyIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} - -type EstablishRelationshipIntentHandler struct { - conf *conf - data code_data.Provider - antispamGuard *antispam.Guard -} - -func NewEstablishRelationshipIntentHandler(conf *conf, data code_data.Provider, antispamGuard *antispam.Guard) CreateIntentHandler { - return &EstablishRelationshipIntentHandler{ - conf: conf, - data: data, - antispamGuard: antispamGuard, - } -} - -func (h *EstablishRelationshipIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { - typedProtoMetadata := protoMetadata.GetEstablishRelationship() - if typedProtoMetadata == nil { - return errors.New("unexpected metadata proto message") - } - - intentRecord.IntentType = intent.EstablishRelationship - intentRecord.EstablishRelationshipMetadata = &intent.EstablishRelationshipMetadata{ - RelationshipTo: typedProtoMetadata.Relationship.GetDomain().Value, - } - - return nil -} - -func (h *EstablishRelationshipIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { - openAction := actions[0].GetOpenAccount() - if openAction == nil { - // Something is off, send it to validation - return false, nil - } - authority, err := common.NewAccountFromProto(openAction.Authority) - if err != nil { - return false, err - } - - // Ensure the authority is the same, otherwise something is off and this - // intent should be sent off to validation. - existingAccountInfoRecord, err := h.data.GetRelationshipAccountInfoByOwnerAddress(ctx, intentRecord.InitiatorOwnerAccount, intentRecord.EstablishRelationshipMetadata.RelationshipTo) - if err == nil && existingAccountInfoRecord.AuthorityAccount == authority.PublicKey().ToBase58() { - return true, nil - } else if err != account.ErrAccountInfoNotFound { - return false, err - } - return false, nil -} - -func (h *EstablishRelationshipIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { - return &lockableAccounts{}, nil -} - -func (h *EstablishRelationshipIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action, deviceToken *string) error { - typedMetadata := metadata.GetEstablishRelationship() - if typedMetadata == nil { - return errors.New("unexpected metadata proto message") - } - reslationshipTo := typedMetadata.Relationship.GetDomain().Value - - initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) - if err != nil { - return err - } - - // - // Part 1: Intent ID validation - // - - err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) - if err != nil { - return err - } - - // - // Part 2: Antispam checks against the phone number - // - - if !h.conf.disableAntispamChecks.Get(ctx) { - allow, err := h.antispamGuard.AllowEstablishNewRelationship(ctx, initiatiorOwnerAccount, reslationshipTo) - if err != nil { - return err - } else if !allow { - return ErrTooManyNewRelationships - } - } - - // - // Part 3: User accounts must be opened - // - - _, err = h.data.GetLatestIntentByInitiatorAndType(ctx, intent.OpenAccounts, initiatiorOwnerAccount.PublicKey().ToBase58()) - if err == intent.ErrIntentNotFound { - return newIntentDeniedError("open accounts intent not submitted") - } else if err != nil { - return err - } - - // - // Part 4: Relationship identifier validation - // - - asciiBaseDomain, err := thirdparty.GetAsciiBaseDomain(reslationshipTo) - if err != nil || asciiBaseDomain != intentRecord.EstablishRelationshipMetadata.RelationshipTo { - return newIntentValidationError("domain is not an ascii base domain") - } - - // - // Part 5: Validate the owner hasn't already established a relationship - // - - existingAccountInfoRecord, err := h.data.GetRelationshipAccountInfoByOwnerAddress(ctx, initiatiorOwnerAccount.PublicKey().ToBase58(), reslationshipTo) - if err == nil { - return newStaleStateErrorf("existing relationship account exists with authority %s", existingAccountInfoRecord.AuthorityAccount) - } else if err != account.ErrAccountInfoNotFound { - return err - } - - // - // Part 6: Local simulation - // - - simResult, err := LocalSimulation(ctx, h.data, actions) - if err != nil { - return err - } - - // - // Part 7: Validate fee payments - // - - err = validateFeePayments(ctx, h.data, intentRecord, simResult) - if err != nil { - return err - } - - // - // Part 8: Validate the individual actions - // - - return h.validateActions(ctx, initiatiorOwnerAccount, actions) -} - -func (h *EstablishRelationshipIntentHandler) validateActions(ctx context.Context, initiatiorOwnerAccount *common.Account, actions []*transactionpb.Action) error { - if len(actions) != 1 { - return newIntentValidationError("expected 1 action") - } - - openAction := actions[0] - if openAction.GetOpenAccount() == nil { - return newActionValidationError(openAction, "expected an open account action") - } - - if openAction.GetOpenAccount().AccountType != commonpb.AccountType_RELATIONSHIP { - return newActionValidationErrorf(openAction, "account type must be %s", commonpb.AccountType_RELATIONSHIP) - } - - if openAction.GetOpenAccount().Index != 0 { - return newActionValidationError(openAction, "index must be 0") - } - - if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatiorOwnerAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "owner must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) - } - - if bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(openAction, "authority cannot be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) - } - - if err := validateTimelockUnlockStateDoesntExist(ctx, h.data, openAction.GetOpenAccount()); err != nil { - return err - } - - return nil -} - -func (h *EstablishRelationshipIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} - -func (h *EstablishRelationshipIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { - return nil -} - -func validateAllUserAccountsManagedByCode(ctx context.Context, initiatorAccounts []*common.AccountRecords) error { - // Try to unlock *ANY* latest account, and you're done - for _, accountRecords := range initiatorAccounts { - if !accountRecords.IsManagedByCode(ctx) { - return ErrNotManagedByCode - } - } - - return nil -} - -func validateBucketAccountSimulation(accountType commonpb.AccountType, simulation TokenAccountSimulation) error { - publicTransfers := simulation.GetPublicTransfers() - if len(publicTransfers) > 0 { - return newActionValidationError(publicTransfers[0].Action, "bucket account cannot send/receive kin publicly") - } - - withdraws := simulation.GetWithdraws() - if len(withdraws) > 0 { - return newActionValidationError(withdraws[0].Action, "bucket account cannot send/receive kin using withdraw") - } - - bucketSize, ok := bucketSizeByAccountType[accountType] - if !ok { - return errors.New("bucket size is not defined") - } - - for _, transfer := range simulation.Transfers { - if transfer.DeltaQuarks%int64(bucketSize) != 0 { - return newActionValidationErrorf(transfer.Action, "quark amount must be a multiple of %d kin", kin.FromQuarks(bucketSize)) - } - - absDeltaQuarks := transfer.DeltaQuarks - if absDeltaQuarks < 0 { - absDeltaQuarks = -absDeltaQuarks - } - - _, anonymized := allowedBucketQuarkAmounts[uint64(absDeltaQuarks)] - if !anonymized { - return newActionValidationError(transfer.Action, "quark amount must be anonymized") - } - } - - return nil -} - -func validateMoneyMovementActionCount(actions []*transactionpb.Action) error { - var numMoneyMovementActions int - - for _, action := range actions { - switch action.Type.(type) { - case *transactionpb.Action_NoPrivacyWithdraw, - *transactionpb.Action_NoPrivacyTransfer, - *transactionpb.Action_TemporaryPrivacyTransfer, - *transactionpb.Action_TemporaryPrivacyExchange, - *transactionpb.Action_FeePayment: - - numMoneyMovementActions++ - } - } - - // todo: configurable - if numMoneyMovementActions > 50 { - return newIntentDeniedError("too many transfer/exchange/withdraw actions") - } - return nil -} - -// Provides generic and lightweight validation of which accounts owned by a Code -// user can be used in certain actions. This is by no means a comprehensive check. -// Other account types (eg. gift cards, external wallets, etc) and intent-specific -// complex nuances should be handled elsewhere. -func validateMoneyMovementActionUserAccounts( - intentType intent.Type, - initiatorAccountsByVault map[string]*common.AccountRecords, - actions []*transactionpb.Action, -) error { - for _, action := range actions { - var authority, source *common.Account - var err error - - switch typedAction := action.Type.(type) { - case *transactionpb.Action_NoPrivacyTransfer: - // No privacy transfers are always come from a deposit account - - authority, err = common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Authority) - if err != nil { - return err - } - - source, err = common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Source) - if err != nil { - return err - } - - sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] - if !ok || (sourceAccountInfo.General.AccountType != commonpb.AccountType_PRIMARY && sourceAccountInfo.General.AccountType != commonpb.AccountType_RELATIONSHIP) { - return newActionValidationError(action, "source account must be a deposit account") - } - case *transactionpb.Action_NoPrivacyWithdraw: - // No privacy withdraws are used in two ways depending on the intent: - // 1. As the sender of funds from the latest temp outgoing account in a private send - // 2. As a receiver of funds to the latest temp incoming account in a public receive - - authority, err = common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Authority) - if err != nil { - return err - } - - source, err = common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Source) - if err != nil { - return err - } - - destination, err := common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Destination) - if err != nil { - return err - } - - switch intentType { - case intent.SendPrivatePayment: - sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] - if !ok || sourceAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_OUTGOING { - return newActionValidationError(action, "source account must be the latest temporary outgoing account") - } - case intent.ReceivePaymentsPublicly: - destinationAccountInfo, ok := initiatorAccountsByVault[destination.PublicKey().ToBase58()] - if !ok || destinationAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { - return newActionValidationError(action, "source account must be the latest temporary incoming account") - } - } - case *transactionpb.Action_TemporaryPrivacyTransfer: - // Temporary privacy transfers are always between Code user accounts - // where one of the source or destination account is a bucket. - - authority, err = common.NewAccountFromProto(typedAction.TemporaryPrivacyTransfer.Authority) - if err != nil { - return err - } - - source, err = common.NewAccountFromProto(typedAction.TemporaryPrivacyTransfer.Source) - if err != nil { - return err - } - - destination, err := common.NewAccountFromProto(typedAction.TemporaryPrivacyTransfer.Destination) - if err != nil { - return err - } - - sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] - if !ok { - return newActionValidationError(action, "source account is not a latest owned account") - } - - destinationAccountInfo, ok := initiatorAccountsByVault[destination.PublicKey().ToBase58()] - if !ok { - return newActionValidationError(action, "destination account is not a latest owned account") - } - - if sourceAccountInfo.General.IsBucket() && destinationAccountInfo.General.IsBucket() { - return newActionValidationError(action, "expected an exchange action") - } - - if !sourceAccountInfo.General.IsBucket() && !destinationAccountInfo.General.IsBucket() { - return newActionValidationError(action, "bucket account must be involved in a private transfer") - } - - if !sourceAccountInfo.General.IsBucket() && - sourceAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_INCOMING && - sourceAccountInfo.General.AccountType != commonpb.AccountType_PRIMARY && - sourceAccountInfo.General.AccountType != commonpb.AccountType_RELATIONSHIP { - return newActionValidationError(action, "source account must be a deposit or latest temporary incoming account when not a bucket account") - } - - if !destinationAccountInfo.General.IsBucket() && destinationAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_OUTGOING { - return newActionValidationError(action, "destination account must be the latest temporary outgoing account when not a bucket account") - } - case *transactionpb.Action_TemporaryPrivacyExchange: - // Temporary privacy exchanges are always between bucket accounts - - authority, err = common.NewAccountFromProto(typedAction.TemporaryPrivacyExchange.Authority) - if err != nil { - return err - } - - source, err = common.NewAccountFromProto(typedAction.TemporaryPrivacyExchange.Source) - if err != nil { - return err - } - - destination, err := common.NewAccountFromProto(typedAction.TemporaryPrivacyExchange.Destination) - if err != nil { - return err - } - - sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] - if !ok || !sourceAccountInfo.General.IsBucket() { - return newActionValidationError(action, "source account must be an owned bucket account") - } - - destinationAccountInfo, ok := initiatorAccountsByVault[destination.PublicKey().ToBase58()] - if !ok || !destinationAccountInfo.General.IsBucket() { - return newActionValidationError(action, "destination account must be an owned bucket account") - } - case *transactionpb.Action_FeePayment: - // Fee payments always come from the latest temporary outgoing account - - authority, err = common.NewAccountFromProto(typedAction.FeePayment.Authority) - if err != nil { - return err - } - - source, err = common.NewAccountFromProto(typedAction.FeePayment.Source) - if err != nil { - return err - } - - sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] - if !ok || sourceAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_OUTGOING { - return newActionValidationError(action, "source account must be the latest temporary outgoing account") - } - default: - continue - } - - expectedTimelockVault, err := getExpectedTimelockVaultFromProtoAccount(authority.ToProto()) - if err != nil { - return err - } else if !bytes.Equal(expectedTimelockVault.PublicKey().ToBytes(), source.PublicKey().ToBytes()) { - return newActionValidationErrorf(action, "authority is invalid") - } - } - - return nil -} - -func validateNextTemporaryAccountOpened( - accountType commonpb.AccountType, - initiatorOwnerAccount *common.Account, - initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, - actions []*transactionpb.Action, -) error { - if accountType != commonpb.AccountType_TEMPORARY_INCOMING && accountType != commonpb.AccountType_TEMPORARY_OUTGOING { - return errors.New("unexpected account type") - } - - prevTempAccountRecords, ok := initiatorAccountsByType[accountType] - if !ok { - return errors.New("previous temp account record missing") - } - - // Find the open and close actions - - var openAction *transactionpb.Action - for _, action := range actions { - switch typed := action.Type.(type) { - case *transactionpb.Action_OpenAccount: - if typed.OpenAccount.AccountType == accountType { - if openAction != nil { - return newIntentValidationErrorf("multiple open actions for %s account type", accountType) - } - - openAction = action - } - } - } - - if openAction == nil { - return newIntentValidationErrorf("open account action for %s account type missing", accountType) - } - - if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatorOwnerAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "owner must be %s", initiatorOwnerAccount.PublicKey().ToBase58()) - } - - if bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(openAction, "authority cannot be %s", initiatorOwnerAccount.PublicKey().ToBase58()) - } - - expectedIndex := prevTempAccountRecords[0].General.Index + 1 - if openAction.GetOpenAccount().Index != expectedIndex { - return newActionValidationErrorf(openAction, "next derivation expected to be %d", expectedIndex) - } - - expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) - if err != nil { - return err - } - - if !bytes.Equal(openAction.GetOpenAccount().Token.Value, expectedVaultAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) - } - - return nil -} - -// Assumes only one gift card account is opened per intent -func validateGiftCardAccountOpened( - ctx context.Context, - data code_data.Provider, - initiatorOwnerAccount *common.Account, - initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, - expectedGiftCardVault *common.Account, - actions []*transactionpb.Action, -) error { - var openAction *transactionpb.Action - for _, action := range actions { - switch typed := action.Type.(type) { - case *transactionpb.Action_OpenAccount: - if typed.OpenAccount.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - if openAction != nil { - return newIntentValidationErrorf("multiple open actions for %s account type", commonpb.AccountType_REMOTE_SEND_GIFT_CARD) - } - - openAction = action - } - } - } - - if openAction == nil { - return newIntentValidationErrorf("open account action for %s account type missing", commonpb.AccountType_REMOTE_SEND_GIFT_CARD) - } - - if bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatorOwnerAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "owner cannot be %s", initiatorOwnerAccount.PublicKey().ToBase58()) - } - - if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { - return newActionValidationErrorf(openAction, "authority must be %s", openAction.GetOpenAccount().Owner.Value) - } - - if openAction.GetOpenAccount().Index != 0 { - return newActionValidationError(openAction, "index must be 0") - } - - derivedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) - if err != nil { - return err - } - - if !bytes.Equal(expectedGiftCardVault.PublicKey().ToBytes(), derivedVaultAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "token must be %s", expectedGiftCardVault.PublicKey().ToBase58()) - } - - if !bytes.Equal(openAction.GetOpenAccount().Token.Value, derivedVaultAccount.PublicKey().ToBytes()) { - return newActionValidationErrorf(openAction, "token must be %s", derivedVaultAccount.PublicKey().ToBase58()) - } - - if err := validateTimelockUnlockStateDoesntExist(ctx, data, openAction.GetOpenAccount()); err != nil { - return err - } - - return nil -} - -func validateNoUpgradeActions(actions []*transactionpb.Action) error { - for _, action := range actions { - switch action.Type.(type) { - case *transactionpb.Action_PermanentPrivacyUpgrade: - return newActionValidationError(action, "update action not allowed") - } - } - - return nil -} - -func validateExternalKinTokenAccountWithinIntent(ctx context.Context, data code_data.Provider, tokenAccount *common.Account) error { - isValid, message, err := common.ValidateExternalKinTokenAccount(ctx, data, tokenAccount) - if err != nil { - return err - } else if !isValid { - return newIntentValidationError(message) - } - return nil -} - -func validateExchangeDataWithinIntent(ctx context.Context, data code_data.Provider, intentId string, proto *transactionpb.ExchangeData) error { - // If there's a payment request record, then validate exchange data client - // provided matches exactly. The payment request record should already have - // validated exchange data before it was created. - requestRecord, err := data.GetRequest(ctx, intentId) - if err == nil { - if !requestRecord.RequiresPayment() { - return newIntentValidationError("request doesn't require payment") - } - - if proto.Currency != string(*requestRecord.ExchangeCurrency) { - return newIntentValidationErrorf("payment has a request for %s currency", *requestRecord.ExchangeCurrency) - } - - absNativeAmountDiff := math.Abs(proto.NativeAmount - *requestRecord.NativeAmount) - if absNativeAmountDiff > 0.0001 { - return newIntentValidationErrorf("payment has a request for %.2f native amount", *requestRecord.NativeAmount) - } - - // No need to validate exchange details in the payment request. Only Kin has - // exact exchange data requirements, which has already been validated at time - // of payment intent creation. We do leave the ability open to reserve an exchange - // rate, but no use cases warrant that atm. - - } else if err != paymentrequest.ErrPaymentRequestNotFound { - return err - } - - // Otherwise, validate exchange data fully using the common method - isValid, message, err := exchange_rate_util.ValidateClientExchangeData(ctx, data, proto) - if err != nil { - return err - } else if !isValid { - if strings.Contains(message, "stale") { - return newStaleStateError(message) - } - return newIntentValidationError(message) - } - return nil -} - -// Generically validates fee payments as much as possible, but won't cover any -// intent-specific nuances (eg. where the fee payment comes from) -// -// This assumes source and destination accounts interacting with fees and the -// remaining amount don't have minimum bucket size requirements. Intent validation -// logic is responsible for these checks and guarantees. -func validateFeePayments( - ctx context.Context, - data code_data.Provider, - intentRecord *intent.Record, - simResult *LocalSimulationResult, -) error { - var requiresFee bool - switch intentRecord.IntentType { - case intent.SendPrivatePayment: - requiresFee = intentRecord.SendPrivatePaymentMetadata.IsMicroPayment - } - - if !requiresFee && simResult.HasAnyFeePayments() { - return newIntentValidationError("intent doesn't require a fee payment") - } - - if requiresFee && !simResult.HasAnyFeePayments() { - return newIntentValidationError("intent requires a fee payment") - } - - if !requiresFee { - return nil - } - - requestRecord, err := data.GetRequest(ctx, intentRecord.IntentId) - if err != nil { - return err - } - - if !requestRecord.RequiresPayment() { - return newIntentValidationError("request doesn't require payment") - } - - additionalRequestedFees := requestRecord.Fees - - feePayments := simResult.GetFeePayments() - if len(feePayments) != len(additionalRequestedFees)+1 { - return newIntentValidationErrorf("expected %d fee payment action", len(additionalRequestedFees)+1) - } - - codeFeePayment := feePayments[0] - - if codeFeePayment.Action.GetFeePayment().Type != transactionpb.FeePaymentAction_CODE { - return newActionValidationError(codeFeePayment.Action, "fee payment type must be CODE") - } - - if codeFeePayment.Action.GetFeePayment().Destination != nil { - return newActionValidationError(codeFeePayment.Action, "code fee payment destination is configured by server") - } - - feeAmount := codeFeePayment.DeltaQuarks - if feeAmount >= 0 { - return newActionValidationError(codeFeePayment.Action, "fee payment amount is negative") - } - feeAmount = -feeAmount // Because it's coming out of a user account in this simulation - - var foundUsdExchangeRecord bool - usdExchangeRecords, err := exchange_rate_util.GetPotentialClientExchangeRates(ctx, data, currency_lib.USD) - if err != nil { - return err - } - for _, exchangeRecord := range usdExchangeRecords { - usdValue := exchangeRecord.Rate * float64(feeAmount) / float64(kin.QuarksPerKin) - - // Allow for some small margin of error - // - // todo: Hardcoded as a penny USD, but might want a dynamic amount if we - // have use cases with different fee amounts. - if usdValue > 0.0099 && usdValue < 0.0101 { - foundUsdExchangeRecord = true - break - } - } - - if !foundUsdExchangeRecord { - return newActionValidationError(codeFeePayment.Action, "code fee payment amount must be $0.01 USD") - } - - for i, additionalFee := range feePayments[1:] { - if additionalFee.Action.GetFeePayment().Type != transactionpb.FeePaymentAction_THIRD_PARTY { - return newActionValidationError(additionalFee.Action, "fee payment type must be THIRD_PARTY") - } - - destination := additionalFee.Action.GetFeePayment().Destination - if destination == nil { - return newActionValidationError(additionalFee.Action, "fee payment destination is required") - } - - // The destination should already be validated as a valid payment destination - if base58.Encode(destination.Value) != additionalRequestedFees[i].DestinationTokenAccount { - return newActionValidationErrorf(additionalFee.Action, "fee payment destination must be %s", additionalRequestedFees[i].DestinationTokenAccount) - } - - feeAmount := additionalFee.DeltaQuarks - if feeAmount >= 0 { - return newActionValidationError(additionalFee.Action, "fee payment amount is negative") - } - feeAmount = -feeAmount // Because it's coming out of a user account in this simulation - - requestedAmount := (uint64(additionalRequestedFees[i].BasisPoints) * intentRecord.SendPrivatePaymentMetadata.Quantity) / 10000 - if feeAmount != int64(requestedAmount) { - return newActionValidationErrorf(additionalFee.Action, "fee payment amount must be for %d bps of total amount", additionalRequestedFees[i].BasisPoints) - } - } - - return nil -} - -func validateMinimalTempIncomingAccountUsage(ctx context.Context, data code_data.Provider, accountInfo *account.Record) error { - if accountInfo.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { - return errors.New("expected a temporary incoming account") - } - - actionRecords, err := data.GetAllActionsByAddress(ctx, accountInfo.TokenAccount) - if err != nil && err != action.ErrActionNotFound { - return err - } - - var paymentCount int - for _, actionRecord := range actionRecords { - // Revoked actions don't count - if actionRecord.State == action.StateRevoked { - continue - } - - // Temp incoming accounts are always paid via no privacy withdraws - if actionRecord.ActionType != action.NoPrivacyWithdraw { - continue - } - - paymentCount += 1 - } - - // Should be coordinated with MustRotate flag in GetTokenAccountInfos - // - // todo: configurable - if paymentCount >= 2 { - // Important Note: Do not leak anything. Just say it isn't the latest. - return newStaleStateError("destination is not the latest temporary incoming account") - } - return nil -} - -func validateClaimedGiftCard(ctx context.Context, data code_data.Provider, giftCardVaultAccount *common.Account, claimedAmount uint64) error { - // - // Part 1: Is the account a gift card? - // - - accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, giftCardVaultAccount.PublicKey().ToBase58()) - if err == account.ErrAccountInfoNotFound || accountInfoRecord.AccountType != commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - return newIntentValidationError("source is not a remote send gift card") - } - - // - // Part 2: Is there already an action to claim the gift card balance? - // - - _, err = data.GetGiftCardClaimedAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) - if err == nil { - return newStaleStateError("gift card balance has already been claimed") - } else if err == action.ErrActionNotFound { - // No action to claim it, so we can proceed - } else if err != nil { - return err - } - - // - // Part 3: Is the action to auto-return the balance back to the issuer in a scheduling or post-scheduling state? - // - - autoReturnActionRecord, err := data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) - if err != nil && err != action.ErrActionNotFound { - return err - } - - if err == nil && autoReturnActionRecord.State != action.StateUnknown { - return newStaleStateError("gift card is expired") - } - - // - // Part 4: Is the gift card managed by Code? - // - - timelockRecord, err := data.GetTimelockByVault(ctx, giftCardVaultAccount.PublicKey().ToBase58()) - if err != nil { - return err - } - - isManagedByCode := common.IsManagedByCode(ctx, timelockRecord) - if err != nil { - return err - } else if !isManagedByCode { - if timelockRecord.IsClosed() { - // Better error messaging, since we know we'll never reopen the account - // and the balance is guaranteed to be claimed (not necessarily through - // Code server though). - return newStaleStateError("gift card balance has already been claimed") - } - return ErrNotManagedByCode - } - - // - // Part 5: Is the full amount being claimed? - // - - // We don't track external deposits to gift cards or any further Code transfers - // to it in SubmitIntent, so this check is sufficient for now. - giftCardBalance, err := balance.CalculateFromCache(ctx, data, giftCardVaultAccount) - if err != nil { - return err - } else if giftCardBalance == 0 { - // Shouldn't be hit with checks from part 3, but left for completeness - return newStaleStateError("gift card balance has already been claimed") - } else if giftCardBalance != claimedAmount { - return newIntentValidationErrorf("must receive entire gift card balance of %d quarks", giftCardBalance) - } - - // - // Part 6: Are we within the threshold for auto-return back to the issuer? - // - - // todo: I think we use the same trick of doing deadline - x minutes to avoid race - // conditions without distributed locks. - if time.Since(accountInfoRecord.CreatedAt) > 24*time.Hour-15*time.Minute { - return newStaleStateError("gift card is expired") - } - - return nil -} - -func validateIntentIdIsNotRequest(ctx context.Context, data code_data.Provider, intentId string) error { - _, err := data.GetRequest(ctx, intentId) - if err == nil { - return newIntentDeniedError("intent id is reserved for a request") - } else if err != paymentrequest.ErrPaymentRequestNotFound { - return err - } - return nil -} - -func validateTipDestination(ctx context.Context, data code_data.Provider, tippedUser *transactionpb.TippedUser, actualDestination *common.Account) error { - var expectedDestination *common.Account - switch tippedUser.Platform { - case transactionpb.TippedUser_TWITTER: - record, err := data.GetTwitterUserByUsername(ctx, tippedUser.Username) - if err == twitter.ErrUserNotFound { - return newIntentValidationError("twitter user is not registered with code") - } else if err != nil { - return err - } - - expectedDestination, err = common.NewAccountFromPublicKeyString(record.TipAddress) - if err != nil { - return err - } - default: - return newIntentValidationErrorf("tip platform %s is not supported", tippedUser.Platform.String()) - } - - if !bytes.Equal(expectedDestination.PublicKey().ToBytes(), actualDestination.PublicKey().ToBytes()) { - return newIntentValidationErrorf("tip destination must be %s", expectedDestination.PublicKey().ToBase58()) - } - - return nil -} - -func validateTimelockUnlockStateDoesntExist(ctx context.Context, data code_data.Provider, openAction *transactionpb.OpenAccountAction) error { - authorityAccount, err := common.NewAccountFromProto(openAction.Authority) - if err != nil { - return err - } - - timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return err - } - - _, err = data.GetBlockchainAccountInfo(ctx, timelockAccounts.Unlock.PublicKey().ToBase58(), solana.CommitmentFinalized) - switch err { - case nil: - return newIntentDeniedError("an account being opened has already initiated an unlock") - case solana.ErrNoAccountInfo: - return nil - default: - return err - } -} - -func getExpectedTimelockVaultFromProtoAccount(authorityProto *commonpb.SolanaAccountId) (*common.Account, error) { - authorityAccount, err := common.NewAccountFromProto(authorityProto) - if err != nil { - return nil, err - } - - timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - if err != nil { - return nil, err - } - return timelockAccounts.Vault, nil -} diff --git a/pkg/code/server/grpc/transaction/v2/limits.go b/pkg/code/server/grpc/transaction/v2/limits.go deleted file mode 100644 index ee08d1c7..00000000 --- a/pkg/code/server/grpc/transaction/v2/limits.go +++ /dev/null @@ -1,223 +0,0 @@ -package transaction_v2 - -import ( - "context" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - - "github.com/code-payments/code-server/pkg/code/balance" - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/phone" - exchange_rate_util "github.com/code-payments/code-server/pkg/code/exchangerate" - "github.com/code-payments/code-server/pkg/code/limit" - currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/kin" -) - -func (s *transactionServer) GetLimits(ctx context.Context, req *transactionpb.GetLimitsRequest) (*transactionpb.GetLimitsResponse, error) { - log := s.log.WithField("method", "GetLimits") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - sig := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, ownerAccount, req, sig); err != nil { - return nil, err - } - - zeroSendLimits := make(map[string]*transactionpb.SendLimit) - zeroMicroPaymentLimits := make(map[string]*transactionpb.MicroPaymentLimit) - zeroBuyModuleLimits := make(map[string]*transactionpb.BuyModuleLimit) - for currency := range limit.SendLimits { - zeroSendLimits[string(currency)] = &transactionpb.SendLimit{ - NextTransaction: 0, - MaxPerTransaction: 0, - MaxPerDay: 0, - } - zeroBuyModuleLimits[string(currency)] = &transactionpb.BuyModuleLimit{ - MaxPerTransaction: 0, - MinPerTransaction: 0, - } - } - for currency := range limit.MicroPaymentLimits { - zeroMicroPaymentLimits[string(currency)] = &transactionpb.MicroPaymentLimit{ - MaxPerTransaction: 0, - MinPerTransaction: 0, - } - } - zeroSendLimits[string(currency_lib.KIN)] = &transactionpb.SendLimit{ - NextTransaction: 0, - MaxPerTransaction: 0, - MaxPerDay: 0, - } - zeroMicroPaymentLimits[string(currency_lib.KIN)] = &transactionpb.MicroPaymentLimit{ - MaxPerTransaction: 0, - MinPerTransaction: 0, - } - zeroResp := &transactionpb.GetLimitsResponse{ - Result: transactionpb.GetLimitsResponse_OK, - SendLimitsByCurrency: zeroSendLimits, - MicroPaymentLimitsByCurrency: zeroMicroPaymentLimits, - BuyModuleLimitsByCurrency: zeroBuyModuleLimits, - DepositLimit: &transactionpb.DepositLimit{ - MaxQuarks: 0, - }, - } - - verificationRecord, err := s.data.GetLatestPhoneVerificationForAccount(ctx, ownerAccount.PublicKey().ToBase58()) - if err == phone.ErrVerificationNotFound { - // We've detected an account that's not verified, so set all limits to 0 - return zeroResp, nil - } else if err != nil { - log.WithError(err).Warn("failure getting phone verification record") - return nil, status.Error(codes.Internal, "") - } - - _, consumedUsdForPayments, err := s.data.GetTransactedAmountForAntiMoneyLaundering(ctx, verificationRecord.PhoneNumber, req.ConsumedSince.AsTime()) - if err != nil { - log.WithError(err).Warn("failure calculating consumed usd payment value") - return nil, status.Error(codes.Internal, "") - } - - _, consumedUsdForDeposits, err := s.data.GetDepositedAmountForAntiMoneyLaundering(ctx, verificationRecord.PhoneNumber, req.ConsumedSince.AsTime()) - if err != nil { - log.WithError(err).Warn("failure calculating consumed usd payment value") - return nil, status.Error(codes.Internal, "") - } - - privateBalance, err := balance.GetPrivateBalance(ctx, s.data, ownerAccount) - if err == balance.ErrNotManagedByCode { - // We've detected an account that's not managed by code, so set all limits to 0 - return zeroResp, nil - } else if err != nil { - log.WithError(err).Warn("failure calculating owner account private balance") - return nil, status.Error(codes.Internal, "") - } - - multiRateRecord, err := s.data.GetAllExchangeRates(ctx, exchange_rate_util.GetLatestExchangeRateTime()) - if err != nil { - log.WithError(err).Warn("failure getting current exchange rates") - return nil, status.Error(codes.Internal, "") - } - - usdRate, ok := multiRateRecord.Rates[string(currency_lib.USD)] - if !ok { - log.WithError(err).Warn("usd rate is missing") - return nil, status.Error(codes.Internal, "") - } - - // - // Part 1: Calculate send limits - // - - sendLimits := make(map[string]*transactionpb.SendLimit) - for currency, sendLimit := range limit.SendLimits { - otherRate, ok := multiRateRecord.Rates[string(currency)] - if !ok { - log.WithError(err).Warnf("%s rate is missing", currency) - continue - } - - // How much have we consumed in the other currency? - consumedInOtherCurrency := consumedUsdForPayments * otherRate / usdRate - - // How much of the daily limit is remaining? - remainingDaily := sendLimit.Daily - consumedInOtherCurrency - - // The per-transaction limit applies up until our remaining daily limit is below it. - remainingNextTransaction := sendLimit.PerTransaction - if remainingDaily < remainingNextTransaction { - remainingNextTransaction = remainingDaily - } - - // Avoid negative limits, possibly caused by fluctuating exchange rates - if remainingNextTransaction < 0 { - remainingNextTransaction = 0 - } - - sendLimits[string(currency)] = &transactionpb.SendLimit{ - NextTransaction: float32(remainingNextTransaction), - MaxPerTransaction: float32(sendLimit.PerTransaction), - MaxPerDay: float32(sendLimit.Daily), - } - } - - usdSendLimits := sendLimits[string(currency_lib.USD)] - - // Inject a Kin limit based on the remaining USD amount and rate - sendLimits[string(currency_lib.KIN)] = &transactionpb.SendLimit{ - NextTransaction: usdSendLimits.NextTransaction / float32(usdRate), - MaxPerTransaction: usdSendLimits.MaxPerTransaction / float32(usdRate), - MaxPerDay: usdSendLimits.MaxPerDay / float32(usdRate), - } - - // - // Part 2: Calculate micro payment limits - // - - microPaymentLimits := make(map[string]*transactionpb.MicroPaymentLimit) - for currency, limits := range limit.MicroPaymentLimits { - microPaymentLimits[string(currency)] = &transactionpb.MicroPaymentLimit{ - MaxPerTransaction: float32(limits.Max), - MinPerTransaction: float32(limits.Min), - } - } - - // - // Part 3: Calculate buy module limits - // - - buyModuleLimits := make(map[string]*transactionpb.BuyModuleLimit) - for currency, limits := range limit.SendLimits { - buyModuleLimits[string(currency)] = &transactionpb.BuyModuleLimit{ - MaxPerTransaction: float32(limits.PerTransaction), - MinPerTransaction: float32(limits.PerTransaction / 10), - } - } - - // - // Part 4: Calculate deposit limits - // - - usdForNextDeposit := limit.MaxPerDepositUsdAmount - - // Does the user already have sufficient balance in their organizer? If so, - // then the limit is completely nullified. - maxPerDepositQuarkAmount := kin.ToQuarks(uint64(limit.MaxPerDepositUsdAmount / usdRate)) - if privateBalance >= maxPerDepositQuarkAmount { - usdForNextDeposit = 0 - } - - // How much of the daily limit is remaining? - remainingUsdForDeposits := limit.MaxDailyDepositUsdAmount - consumedUsdForDeposits - - // The per-transaction limit applies up until our remaining daily limit is below it. - if remainingUsdForDeposits < usdForNextDeposit { - usdForNextDeposit = remainingUsdForDeposits - } - - // Avoid negative limits, possibly caused by fluctuating exchange rates - if usdForNextDeposit < 0 { - usdForNextDeposit = 0 - } - - return &transactionpb.GetLimitsResponse{ - Result: transactionpb.GetLimitsResponse_OK, - SendLimitsByCurrency: sendLimits, - MicroPaymentLimitsByCurrency: microPaymentLimits, - BuyModuleLimitsByCurrency: buyModuleLimits, - DepositLimit: &transactionpb.DepositLimit{ - MaxQuarks: kin.ToQuarks(uint64(usdForNextDeposit / usdRate)), - }, - }, nil -} diff --git a/pkg/code/server/grpc/transaction/v2/proof.go b/pkg/code/server/grpc/transaction/v2/proof.go deleted file mode 100644 index 46195d76..00000000 --- a/pkg/code/server/grpc/transaction/v2/proof.go +++ /dev/null @@ -1,271 +0,0 @@ -package transaction_v2 - -import ( - "context" - "encoding/hex" - "errors" - "sync" - "time" - - "github.com/mr-tron/base58" - - commitment_worker "github.com/code-payments/code-server/pkg/code/async/commitment" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" - "github.com/code-payments/code-server/pkg/code/data/merkletree" -) - -type refreshingMerkleTree struct { - tree *merkletree.MerkleTree - lastRefreshedAt time.Time -} - -var ( - merkleTreeLock sync.Mutex - cachedMerkleTrees map[string]*refreshingMerkleTree -) - -func init() { - cachedMerkleTrees = make(map[string]*refreshingMerkleTree) -} - -type privacyUpgradeProof struct { - proof []merkletree.Hash - - newCommitment *common.Account - newCommitmentVault *common.Account - newCommitmentTranscript merkletree.Hash - newCommitmentDestination *common.Account - newCommitmentAmount uint64 - newCommitmentRoot merkletree.Hash -} - -type privacyUpgradeCandidate struct { - newCommitmentRecord *commitment.Record - - forLeafHash merkletree.Hash - forLeaf uint64 - - recentRoot merkletree.Hash - untilLeaf uint64 -} - -func canUpgradeCommitmentAction(ctx context.Context, data code_data.Provider, commitmentRecord *commitment.Record) (bool, error) { - _, err := selectCandidateForPrivacyUpgrade(ctx, data, commitmentRecord.Intent, commitmentRecord.ActionId) - switch err { - case ErrPrivacyAlreadyUpgraded, ErrWaitForNextBlock, ErrPrivacyUpgradeMissed: - return false, nil - case nil: - return true, nil - default: - return false, err - } -} - -// Note: How we get select commmitments for proofs plays into how we decide to -// close commitment vaults. Updates to logic should be in sync. -func selectCandidateForPrivacyUpgrade(ctx context.Context, data code_data.Provider, intentId string, actionId uint32) (*privacyUpgradeCandidate, error) { - actionRecord, err := data.GetActionById(ctx, intentId, actionId) - if err != nil { - return nil, err - } - if actionRecord.ActionType != action.PrivateTransfer { - return nil, ErrInvalidActionToUpgrade - } - - switch actionRecord.State { - case action.StateUnknown: - // Action isn't scheduled, so no fulfillments were either. Keep waiting. - return nil, ErrWaitForNextBlock - case action.StateRevoked: - // Aciton is revoked, so it can never be upgraded. - return nil, ErrInvalidActionToUpgrade - case action.StateFailed, action.StateConfirmed: - // Because commitmentRecord.RepaymentDivertedTo is nil, we must have submitted - // the temporary private transfer. - return nil, ErrPrivacyUpgradeMissed - } - - commitmentRecord, err := data.GetCommitmentByAction(ctx, intentId, actionId) - if err != nil { - return nil, err - } - if commitmentRecord.RepaymentDivertedTo != nil { - // The private payment has already been upgraded - return nil, ErrPrivacyAlreadyUpgraded - } - - privacyUpgradeDeadline, err := commitment_worker.GetDeadlineToUpgradePrivacy(ctx, data, commitmentRecord) - switch err { - case nil: - // We're too close to the privacy upgrade deadline, so mark it as missed - // to avoid any kind of races. - if privacyUpgradeDeadline.Add(-5 * time.Minute).Before(time.Now()) { - return nil, ErrPrivacyUpgradeMissed - } - case commitment_worker.ErrNoPrivacyUpgradeDeadline: - default: - return nil, err - } - - merkleTree, err := getCachedMerkleTreeForTreasury(ctx, data, commitmentRecord.Pool) - if err != nil { - return nil, err - } - - commitmentAddressBytes, err := base58.Decode(commitmentRecord.Address) - if err != nil { - return nil, err - } - - commitmentLeafNode, err := merkleTree.GetLeafNode(ctx, commitmentAddressBytes) - if err == merkletree.ErrLeafNotFound { - // The commitment isn't in the merkle tree, so we need to wait to observe it - // added as a leaf from the blockchain state. - return nil, ErrWaitForNextBlock - } else if err != nil { - return nil, err - } - - // Note: Assumes we've coded SubmitIntent and the scheduling layer to ensure - // we create new commitments with an ever-progressing recent root that is the - // latest value. - latestLeafNode, err := merkleTree.GetLastAddedLeafNode(ctx) - if err == merkletree.ErrLeafNotFound { - // The merkle tree is empty, which means we're starting off with a fresh - // treasury pool and need to see further state before we can do anything. - return nil, ErrWaitForNextBlock - } else if err != nil { - return nil, err - } - - latestCommitmentRecord, err := data.GetCommitmentByAddress(ctx, base58.Encode(latestLeafNode.LeafValue)) - if err != nil { - return nil, err - } - - recentRootForProof, err := hex.DecodeString(latestCommitmentRecord.RecentRoot) - if err != nil { - return nil, err - } - - recentRootLeafNode, err := merkleTree.GetLeafNodeForRoot(ctx, recentRootForProof) - if err == merkletree.ErrLeafNotFound || err == merkletree.ErrRootNotFound { - // The recent root isn't in the merkle tree, which likely means we're - // starting off with a new treasury pool and haven't observed sufficient - // state. - return nil, ErrWaitForNextBlock - } else if err != nil { - return nil, err - } - - if commitmentLeafNode.Index > recentRootLeafNode.Index { - // The commitment happened on or after the recent root, so we need to wait - // and observe another. - return nil, ErrWaitForNextBlock - } - - return &privacyUpgradeCandidate{ - newCommitmentRecord: latestCommitmentRecord, - - forLeafHash: commitmentAddressBytes, - forLeaf: commitmentLeafNode.Index, - - recentRoot: recentRootForProof, - untilLeaf: recentRootLeafNode.Index, - }, nil -} - -// Note: How we get proofs plays into how we decide to close commitment vaults. Updates to -// logic should be in sync. -func getProofForPrivacyUpgrade(ctx context.Context, data code_data.Provider, upgradingTo *privacyUpgradeCandidate) (*privacyUpgradeProof, error) { - merkleTree, err := getCachedMerkleTreeForTreasury(ctx, data, upgradingTo.newCommitmentRecord.Pool) - if err != nil { - return nil, err - } - - proof, err := merkleTree.GetProofForLeafAtIndex(ctx, upgradingTo.forLeaf, upgradingTo.untilLeaf) - if err != nil { - return nil, err - } - - // Keeping this in as a sanity check, for now. If we can't validate the proof, - // then the client certainly can't either. - if !merkletree.Verify(proof, upgradingTo.recentRoot, merkletree.Leaf(upgradingTo.forLeafHash)) { - return nil, errors.New("proof unexpectedly failed verification") - } - - newCommitmentAccount, err := common.NewAccountFromPublicKeyString(upgradingTo.newCommitmentRecord.Address) - if err != nil { - return nil, err - } - - newCommitmentVaultAccount, err := common.NewAccountFromPublicKeyString(upgradingTo.newCommitmentRecord.VaultAddress) - if err != nil { - return nil, err - } - - newCommitmentDestinationAccount, err := common.NewAccountFromPublicKeyString(upgradingTo.newCommitmentRecord.Destination) - if err != nil { - return nil, err - } - - newCommitmentTranscript, err := hex.DecodeString(upgradingTo.newCommitmentRecord.Transcript) - if err != nil { - return nil, err - } - - return &privacyUpgradeProof{ - proof: proof, - - newCommitment: newCommitmentAccount, - newCommitmentVault: newCommitmentVaultAccount, - newCommitmentDestination: newCommitmentDestinationAccount, - newCommitmentTranscript: newCommitmentTranscript, - newCommitmentAmount: upgradingTo.newCommitmentRecord.Amount, - newCommitmentRoot: upgradingTo.recentRoot, - }, nil -} - -// todo: move this into a common spot? code is duplicated -func getCachedMerkleTreeForTreasury(ctx context.Context, data code_data.Provider, address string) (*merkletree.MerkleTree, error) { - merkleTreeLock.Lock() - defer merkleTreeLock.Unlock() - - cachedTreasuryMetadata, err := getCachedTreasuryMetadataByNameOrAddress(ctx, data, address, 7*31*24*time.Hour) - if err != nil { - return nil, err - } - name := cachedTreasuryMetadata.name - - cached, ok := cachedMerkleTrees[name] - if !ok { - loaded, err := data.LoadExistingMerkleTree(ctx, name, true) - if err != nil { - return nil, err - } - - cached = &refreshingMerkleTree{ - tree: loaded, - lastRefreshedAt: time.Now(), - } - cachedMerkleTrees[name] = cached - } - - // Refresh the merkle tree periodically, instead of loading it every time. - // - // todo: configurable value (put an upper bound when it does become configurable) - // todo: keep this small relative to the commitment worker's close timeout threshold (currently 10m) for the next leaf (until we have distributed locks at least) - if time.Since(cached.lastRefreshedAt) > time.Minute { - err := cached.tree.Refresh(ctx) - if err != nil { - return nil, err - } - - cached.lastRefreshedAt = time.Now() - } - - return cached.tree, nil -} diff --git a/pkg/code/server/grpc/transaction/v2/treasury.go b/pkg/code/server/grpc/transaction/v2/treasury.go deleted file mode 100644 index 99ad3c0d..00000000 --- a/pkg/code/server/grpc/transaction/v2/treasury.go +++ /dev/null @@ -1,248 +0,0 @@ -package transaction_v2 - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/treasury" -) - -// todo: any other treasury-related things we can put here? - -func init() { - cachedTreasuryMetadatas = make(map[string]*cachedTreasuryMetadata) -} - -var ( - treasuryCacheMu sync.RWMutex - cachedTreasuryMetadatas map[string]*cachedTreasuryMetadata -) - -type cachedTreasuryMetadata struct { - name string - - stateAccount *common.Account - stateBump uint8 - - vaultAccount *common.Account - vaultBump uint8 - - mostRecentRoot string - - lastUpdatedAt time.Time -} - -func getCachedTreasuryMetadataByNameOrAddress(ctx context.Context, data code_data.Provider, nameOrAddress string, maxAge time.Duration) (*cachedTreasuryMetadata, error) { - treasuryCacheMu.RLock() - cached, ok := cachedTreasuryMetadatas[nameOrAddress] - if ok && time.Since(cached.lastUpdatedAt) < maxAge { - treasuryCacheMu.RUnlock() - return cached, nil - } - treasuryCacheMu.RUnlock() - - treasuryCacheMu.Lock() - defer treasuryCacheMu.Unlock() - - cached, ok = cachedTreasuryMetadatas[nameOrAddress] - if ok && time.Since(cached.lastUpdatedAt) < maxAge { - return cached, nil - } - - var isName bool - _, err := common.NewAccountFromPublicKeyString(nameOrAddress) - if err != nil { - isName = true - } - - var treasuryPoolRecord *treasury.Record - if isName { - treasuryPoolRecord, err = data.GetTreasuryPoolByName(ctx, nameOrAddress) - if err != nil { - return nil, err - } - } else { - treasuryPoolRecord, err = data.GetTreasuryPoolByAddress(ctx, nameOrAddress) - if err != nil { - return nil, err - } - } - - stateAccount, err := common.NewAccountFromPublicKeyString(treasuryPoolRecord.Address) - if err != nil { - return nil, err - } - - vaultAccount, err := common.NewAccountFromPublicKeyString(treasuryPoolRecord.Vault) - if err != nil { - return nil, err - } - - cached = &cachedTreasuryMetadata{ - name: treasuryPoolRecord.Name, - - stateAccount: stateAccount, - stateBump: treasuryPoolRecord.Bump, - - vaultAccount: vaultAccount, - vaultBump: treasuryPoolRecord.VaultBump, - - mostRecentRoot: treasuryPoolRecord.GetMostRecentRoot(), - - lastUpdatedAt: time.Now(), - } - cachedTreasuryMetadatas[treasuryPoolRecord.Name] = cached - cachedTreasuryMetadatas[treasuryPoolRecord.Address] = cached - return cached, nil -} - -type treasuryPoolStats struct { - totalAvailableFunds uint64 - totalDeficit uint64 - lastUpdatedAt time.Time -} - -// todo: Assumes constant and single treasury pool -func (s *transactionServer) treasuryPoolMonitor(ctx context.Context, name string) { - log := s.log.WithFields(logrus.Fields{ - "method": "treasuryPoolMonitor", - "treasury": name, - }) - - var initialLoadCompleted bool - var treasuryPoolRecord *treasury.Record - var err error - - for { - func() { - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - - start := time.Now() - defer time.Sleep(s.conf.treasuryPoolStatsRefreshInterval.Get(ctx) - time.Since(start)) - - if treasuryPoolRecord == nil { - treasuryPoolRecord, err = s.data.GetTreasuryPoolByName(ctx, name) - if err != nil { - log.WithError(err).Warn("failure getting treasury pool record") - return - } - } - - totalAvailable, err := s.data.GetTotalAvailableTreasuryPoolFunds(ctx, treasuryPoolRecord.Vault) - if err != nil { - log.WithError(err).Warn("failure getting total funds") - return - } - - totalDeficit, err := s.data.GetTotalTreasuryPoolDeficitFromCommitments(ctx, treasuryPoolRecord.Address) - if err != nil { - log.WithError(err).Warn("failure getting total deficit") - return - } - - s.treasuryPoolStatsLock.Lock() - defer s.treasuryPoolStatsLock.Unlock() - - s.currentTreasuryPoolStatsByName[name] = &treasuryPoolStats{ - totalAvailableFunds: totalAvailable, - totalDeficit: totalDeficit, - lastUpdatedAt: time.Now(), - } - - if !initialLoadCompleted { - initialLoadCompleted = true - s.treasuryPoolStatsInitialLoadWgByName[name].Done() - } - }() - } -} - -func (s *transactionServer) selectTreasuryPoolForAdvance(ctx context.Context, quarks uint64) (string, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "selectTreasuryPoolForAdvance", - "quarks": quarks, - }) - - for _, bucket := range []uint64{ - kin.ToQuarks(1_000_000), - kin.ToQuarks(100_000), - kin.ToQuarks(10_000), - kin.ToQuarks(1_000), - kin.ToQuarks(100), - kin.ToQuarks(10), - kin.ToQuarks(1), - } { - // Not a multiple of the bucket - if quarks%bucket != 0 { - continue - } - - // Sanity check for something too large to allow - if quarks > 9*bucket { - continue - } - - return s.treasuryPoolNameByBaseAmount[bucket], nil - } - - // If we reach this point, we must have allowed a bucket multiple that wasn't really allowed - log.Warn("no treasury selected to handle advance") - - return "", errors.New("treasury not available") -} - -func (s *transactionServer) areAllTreasuryPoolsAvailable(ctx context.Context) (bool, error) { - for _, treasuryPoolName := range s.treasuryPoolNameByBaseAmount { - isTreasuryAvailable, err := s.isTreasuryPoolAvailable(ctx, treasuryPoolName) - if err != nil { - return false, err - } else if !isTreasuryAvailable { - return false, nil - } - } - return true, nil -} - -func (s *transactionServer) isTreasuryPoolAvailable(ctx context.Context, name string) (bool, error) { - log := s.log.WithFields(logrus.Fields{ - "method": "isTreasuryPoolAvailable", - "treasury": name, - }) - - s.treasuryPoolStatsInitialLoadWgByName[name].Wait() - - s.treasuryPoolStatsLock.RLock() - defer s.treasuryPoolStatsLock.RUnlock() - - currentStats := s.currentTreasuryPoolStatsByName[name] - - // Have a small threshold where we allow stats to be outdated to allow for blips - if time.Since(currentStats.lastUpdatedAt) >= time.Minute { - log.Warn("treasury pool stats are outdated") - return false, errors.New("treasury pool stats are outdated") - } - - if currentStats.totalDeficit >= currentStats.totalAvailableFunds { - log.Warn("treasury pool isn't available because all funds are accounted for") - return false, nil - } - remaining := currentStats.totalAvailableFunds - currentStats.totalDeficit - - // Try to maintain some available funds to avoid any potential deadlocks. - // If we happen to go slightly over, it's not the end of the world either. - percentAvailable := float64(remaining) / float64(currentStats.totalAvailableFunds) - if percentAvailable < 0.05 { - log.Warn("treasury pool isn't available because most funds are accounted for") - return false, nil - } - - return true, nil -} diff --git a/pkg/code/server/grpc/user/config.go b/pkg/code/server/grpc/user/config.go deleted file mode 100644 index e7f49e83..00000000 --- a/pkg/code/server/grpc/user/config.go +++ /dev/null @@ -1,42 +0,0 @@ -package user - -import ( - "github.com/code-payments/code-server/pkg/config" - "github.com/code-payments/code-server/pkg/config/env" - "github.com/code-payments/code-server/pkg/config/memory" - "github.com/code-payments/code-server/pkg/config/wrapper" -) - -const ( - envConfigPrefix = "USER_SERVICE_" - - EnableBuyModuleConfigEnvName = envConfigPrefix + "ENABLE_BUY_MODULE" - defaultEnableBuyModule = true -) - -type conf struct { - enableBuyModule config.Bool -} - -// ConfigProvider defines how config values are pulled -type ConfigProvider func() *conf - -// WithEnvConfigs returns configuration pulled from environment variables -func WithEnvConfigs() ConfigProvider { - return func() *conf { - return &conf{ - enableBuyModule: env.NewBoolConfig(EnableBuyModuleConfigEnvName, defaultEnableBuyModule), - } - } -} - -type testOverrides struct { -} - -func withManualTestOverrides(overrides *testOverrides) ConfigProvider { - return func() *conf { - return &conf{ - enableBuyModule: wrapper.NewBoolConfig(memory.NewConfig(defaultEnableBuyModule), defaultEnableBuyModule), - } - } -} diff --git a/pkg/code/server/grpc/user/limiter.go b/pkg/code/server/grpc/user/limiter.go deleted file mode 100644 index 9da833e9..00000000 --- a/pkg/code/server/grpc/user/limiter.go +++ /dev/null @@ -1,74 +0,0 @@ -package user - -import ( - "context" - - "github.com/sirupsen/logrus" - - "github.com/code-payments/code-server/pkg/rate" - - grpc_client "github.com/code-payments/code-server/pkg/grpc/client" -) - -// limiter limits user-based service calls by both IP and phone number -type limiter struct { - log *logrus.Entry - ip rate.Limiter - phoneNumber rate.Limiter -} - -// NewLimiter creates a new Limiter. -func newLimiter(ctor rate.LimiterCtor, phoneNumberLimit, ipLimit float64) *limiter { - return &limiter{ - log: logrus.StandardLogger().WithField("type", "user/limiter"), - ip: ctor(ipLimit), - phoneNumber: ctor(phoneNumberLimit), - } -} - -func (l *limiter) allowPhoneLinking(ctx context.Context, phoneNumber string) bool { - log := l.log.WithFields(logrus.Fields{ - "method": "allowPhoneLinking", - "phone": phoneNumber, - }) - - ip, err := grpc_client.GetIPAddr(ctx) - if err != nil { - log.WithError(err).Warn("failure getting client ip") - } else { - log = log.WithField("ip", ip) - allow, err := l.allowIP(ip) - if err != nil { - log.WithError(err).Warn("failure checking ip rate limit") - } else if !allow { - log.Trace("ip is rate limited") - return false - } - } - - allow, err := l.allowPhoneNumber(phoneNumber) - if err != nil { - log.WithError(err).Warn("failure checking phone number rate limit") - } else if !allow { - log.Trace("phone number is rate limited") - return false - } - - return true -} - -func (l *limiter) allowIP(ipAddr string) (bool, error) { - allowed, err := l.ip.Allow(ipAddr) - if err != nil { - return true, err - } - return allowed, nil -} - -func (l *limiter) allowPhoneNumber(phoneNumber string) (bool, error) { - allowed, err := l.phoneNumber.Allow(phoneNumber) - if err != nil { - return true, err - } - return allowed, nil -} diff --git a/pkg/code/server/grpc/user/server.go b/pkg/code/server/grpc/user/server.go deleted file mode 100644 index 1b24adf6..00000000 --- a/pkg/code/server/grpc/user/server.go +++ /dev/null @@ -1,741 +0,0 @@ -package user - -import ( - "context" - "database/sql" - "time" - - "github.com/mr-tron/base58/base58" - "github.com/sirupsen/logrus" - "golang.org/x/text/language" - xrate "golang.org/x/time/rate" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - userpb "github.com/code-payments/code-protobuf-api/generated/go/user/v1" - - "github.com/code-payments/code-server/pkg/code/antispam" - auth_util "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/paymentrequest" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/twitter" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/code/data/user/storage" - "github.com/code-payments/code-server/pkg/code/data/webhook" - "github.com/code-payments/code-server/pkg/code/server/grpc/messaging" - transaction_server "github.com/code-payments/code-server/pkg/code/server/grpc/transaction/v2" - "github.com/code-payments/code-server/pkg/code/thirdparty" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/rate" - "github.com/code-payments/code-server/pkg/sync" -) - -type identityServer struct { - log *logrus.Entry - conf *conf - data code_data.Provider - auth *auth_util.RPCSignatureVerifier - limiter *limiter - antispamGuard *antispam.Guard - messagingClient messaging.InternalMessageClient - domainVerifier thirdparty.DomainVerifier - - // todo: distributed lock - intentLocks *sync.StripedLock - - userpb.UnimplementedIdentityServer -} - -func NewIdentityServer( - data code_data.Provider, - auth *auth_util.RPCSignatureVerifier, - antispamGuard *antispam.Guard, - messagingClient messaging.InternalMessageClient, - configProvider ConfigProvider, -) userpb.IdentityServer { - // todo: don't use a local rate limiter, but it's good enough for now - // todo: these rate limits are arbitrary and might need tuning - limiter := newLimiter(func(r float64) rate.Limiter { - return rate.NewLocalRateLimiter(xrate.Limit(r)) - }, 1, 5) - - return &identityServer{ - log: logrus.StandardLogger().WithField("type", "user/server"), - conf: configProvider(), - data: data, - auth: auth, - limiter: limiter, - antispamGuard: antispamGuard, - messagingClient: messagingClient, - domainVerifier: thirdparty.VerifyDomainNameOwnership, - - intentLocks: sync.NewStripedLock(1024), - } -} - -func (s *identityServer) LinkAccount(ctx context.Context, req *userpb.LinkAccountRequest) (*userpb.LinkAccountResponse, error) { - log := s.log.WithField("method", "LinkAccount") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, ownerAccount, req, signature); err != nil { - return nil, err - } - - var result userpb.LinkAccountResponse_Result - var userID *user.UserID - var dataContainerID *user.DataContainerID - var metadata *userpb.PhoneMetadata - - switch token := req.Token.(type) { - case *userpb.LinkAccountRequest_Phone: - log = log.WithFields(logrus.Fields{ - "phone": token.Phone.PhoneNumber.Value, - "code": token.Phone.Code.Value, - }) - - if !s.limiter.allowPhoneLinking(ctx, token.Phone.PhoneNumber.Value) { - result = userpb.LinkAccountResponse_RATE_LIMITED - break - } - - allow, err := s.antispamGuard.AllowLinkAccount(ctx, ownerAccount, token.Phone.PhoneNumber.Value) - if err != nil { - log.WithError(err).Warn("failure performing antispam checks") - return nil, status.Error(codes.Internal, "") - } else if !allow { - result = userpb.LinkAccountResponse_RATE_LIMITED - break - } - - err = s.data.UsePhoneLinkingToken(ctx, token.Phone.PhoneNumber.Value, token.Phone.Code.Value) - if err == phone.ErrLinkingTokenNotFound { - result = userpb.LinkAccountResponse_INVALID_TOKEN - break - } else if err != nil { - log.WithError(err).Warn("failure using phone linking token") - return nil, status.Error(codes.Internal, "") - } - - falseValue := false - err = s.data.SaveOwnerAccountPhoneSetting(ctx, token.Phone.PhoneNumber.Value, &phone.OwnerAccountSetting{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IsUnlinked: &falseValue, - CreatedAt: time.Now(), - LastUpdatedAt: time.Now(), - }) - if err != nil { - log.WithError(err).Warn("failure enabling remote send setting") - return nil, status.Error(codes.Internal, "") - } - - err = s.data.SavePhoneVerification(ctx, &phone.Verification{ - PhoneNumber: token.Phone.PhoneNumber.Value, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - }) - if err != nil { - log.WithError(err).Warn("failure saving verification record") - return nil, status.Error(codes.Internal, "") - } - - newUser := identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &token.Phone.PhoneNumber.Value, - }, - CreatedAt: time.Now(), - } - err = s.data.PutUser(ctx, &newUser) - if err != identity.ErrAlreadyExists && err != nil { - log.WithError(err).Warn("failure inserting user identity") - return nil, status.Error(codes.Internal, "") - } - - newDataContainer := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &token.Phone.PhoneNumber.Value, - }, - CreatedAt: time.Now(), - } - err = s.data.PutUserDataContainer(ctx, newDataContainer) - if err != storage.ErrAlreadyExists && err != nil { - log.WithError(err).Warn("failure inserting data container") - return nil, status.Error(codes.Internal, "") - } - - existingUser, err := s.data.GetUserByPhoneView(ctx, token.Phone.PhoneNumber.Value) - if err != nil { - log.WithError(err).Warn("failure getting user identity from phone view") - return nil, status.Error(codes.Internal, "") - } - - userID = existingUser.ID - log = log.WithField("user", userID.String()) - - existingDataContainer, err := s.data.GetUserDataContainerByPhone(ctx, ownerAccount.PublicKey().ToBase58(), token.Phone.PhoneNumber.Value) - if err != nil { - log.WithError(err).Warn("failure getting data container for phone") - return nil, status.Error(codes.Internal, "") - } - - dataContainerID = existingDataContainer.ID - - metadata = &userpb.PhoneMetadata{ - IsLinked: true, - } - default: - return nil, status.Error(codes.InvalidArgument, "token must be set") - } - - if result != userpb.LinkAccountResponse_OK { - return &userpb.LinkAccountResponse{ - Result: result, - }, nil - } - - return &userpb.LinkAccountResponse{ - Result: result, - User: &userpb.User{ - Id: userID.Proto(), - View: &userpb.View{ - PhoneNumber: req.GetPhone().PhoneNumber, - }, - }, - DataContainerId: dataContainerID.Proto(), - Metadata: &userpb.LinkAccountResponse_Phone{ - Phone: metadata, - }, - }, nil -} - -func (s *identityServer) UnlinkAccount(ctx context.Context, req *userpb.UnlinkAccountRequest) (*userpb.UnlinkAccountResponse, error) { - log := s.log.WithField("method", "UnlinkAccount") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, ownerAccount, req, signature); err != nil { - return nil, err - } - - result := userpb.UnlinkAccountResponse_OK - switch identifer := req.IdentifyingFeature.(type) { - case *userpb.UnlinkAccountRequest_PhoneNumber: - log = log.WithField("phone", identifer.PhoneNumber.Value) - - _, err := s.data.GetPhoneVerification(ctx, ownerAccount.PublicKey().ToBase58(), identifer.PhoneNumber.Value) - if err == phone.ErrVerificationNotFound { - result = userpb.UnlinkAccountResponse_NEVER_ASSOCIATED - break - } else if err != nil { - log.WithError(err).Warn("failure getting phone verification") - return nil, status.Error(codes.Internal, "") - } - - trueVal := true - err = s.data.SaveOwnerAccountPhoneSetting(ctx, identifer.PhoneNumber.Value, &phone.OwnerAccountSetting{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IsUnlinked: &trueVal, - CreatedAt: time.Now(), - LastUpdatedAt: time.Now(), - }) - if err != nil { - log.WithError(err).Warn("failure disabling remote send setting") - return nil, status.Error(codes.Internal, "") - } - default: - return nil, status.Error(codes.InvalidArgument, "identifying_feature must be set") - } - - return &userpb.UnlinkAccountResponse{ - Result: result, - }, nil -} - -func (s *identityServer) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.GetUserResponse, error) { - log := s.log.WithField("method", "GetUser") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, ownerAccount, req, signature); err != nil { - return nil, err - } - - ownerManagementState, err := common.GetOwnerManagementState(ctx, s.data, ownerAccount) - if err != nil { - log.WithError(err).Warn("failure getting owner management state") - return nil, status.Error(codes.Internal, "") - } - - var result userpb.GetUserResponse_Result - var userID *user.UserID - var isStaff bool - var dataContainerID *user.DataContainerID - var metadata *userpb.PhoneMetadata - - switch identifer := req.IdentifyingFeature.(type) { - case *userpb.GetUserRequest_PhoneNumber: - log = log.WithField("phone", identifer.PhoneNumber.Value) - - user, err := s.data.GetUserByPhoneView(ctx, identifer.PhoneNumber.Value) - if err == identity.ErrNotFound { - result = userpb.GetUserResponse_NOT_FOUND - break - } else if err != nil { - log.WithError(err).Warn("failure getting user identity from phone view") - return nil, status.Error(codes.Internal, "") - } - - userID = user.ID - log = log.WithField("user", userID.String()) - - isStaff = user.IsStaffUser - - // todo: needs a test - if user.IsBanned { - log.Info("banned user login denied") - result = userpb.GetUserResponse_NOT_FOUND - break - } - - dataContainer, err := s.data.GetUserDataContainerByPhone(ctx, ownerAccount.PublicKey().ToBase58(), identifer.PhoneNumber.Value) - if err != nil { - log.WithError(err).Warn("failure getting data container for phone") - return nil, status.Error(codes.Internal, "") - } - - dataContainerID = dataContainer.ID - - if ownerManagementState == common.OwnerManagementStateUnlocked { - result = userpb.GetUserResponse_UNLOCKED_TIMELOCK_ACCOUNT - break - } - - isLinked, err := s.data.IsPhoneNumberLinkedToAccount(ctx, identifer.PhoneNumber.Value, ownerAccount.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting link status to account") - return nil, status.Error(codes.Internal, "") - } - - metadata = &userpb.PhoneMetadata{ - IsLinked: isLinked, - } - default: - return nil, status.Error(codes.InvalidArgument, "identifying_feature must be set") - } - - if result != userpb.GetUserResponse_OK { - return &userpb.GetUserResponse{ - Result: result, - }, nil - } - - // todo: Start centralizing airdrop intent logic somewhere - var eligibleAirdrops []transactionpb.AirdropType - userAgent, err := client.GetUserAgent(ctx) - if err == nil && userAgent.DeviceType == client.DeviceTypeIOS { - eligibleAirdrops = append(eligibleAirdrops, transactionpb.AirdropType_GET_FIRST_KIN) - } - for _, intentId := range []string{ - transaction_server.GetNewAirdropIntentId(transaction_server.AirdropTypeGetFirstKin, ownerAccount.PublicKey().ToBase58()), - transaction_server.GetOldAirdropIntentId(transaction_server.AirdropTypeGetFirstKin, ownerAccount.PublicKey().ToBase58()), - } { - _, err = s.data.GetIntent(ctx, intentId) - if err == nil { - eligibleAirdrops = []transactionpb.AirdropType{} - break - } else if err != intent.ErrIntentNotFound { - log.WithError(err).Warnf("failure checking %s airdrop status", transactionpb.AirdropType_GET_FIRST_KIN) - return nil, status.Error(codes.Internal, "") - } - } - - return &userpb.GetUserResponse{ - Result: result, - User: &userpb.User{ - Id: userID.Proto(), - View: &userpb.View{ - PhoneNumber: req.GetPhoneNumber(), - }, - }, - DataContainerId: dataContainerID.Proto(), - Metadata: &userpb.GetUserResponse_Phone{ - Phone: metadata, - }, - EnableInternalFlags: isStaff, - EligibleAirdrops: eligibleAirdrops, - EnableBuyModule: s.conf.enableBuyModule.Get(ctx), - }, nil -} - -func (s *identityServer) UpdatePreferences(ctx context.Context, req *userpb.UpdatePreferencesRequest) (*userpb.UpdatePreferencesResponse, error) { - log := s.log.WithField("method", "UpdatePreferences") - log = client.InjectLoggingMetadata(ctx, log) - - ownerAccount, err := common.NewAccountFromProto(req.OwnerAccountId) - if err != nil { - log.WithError(err).Warn("owner account is invalid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) - - containerID, err := user.GetDataContainerIDFromProto(req.ContainerId) - if err != nil { - log.WithError(err).Warn("failure parsing data container id as uuid") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("data_container", containerID.String()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.AuthorizeDataAccess(ctx, containerID, ownerAccount, req, signature); err != nil { - return nil, err - } - - locale, err := language.Parse(req.Locale.Value) - if err != nil { - log.WithError(err).Info("client provided an invalid locale") - return &userpb.UpdatePreferencesResponse{ - Result: userpb.UpdatePreferencesResponse_INVALID_LOCALE, - }, nil - } - - record, err := s.data.GetUserPreferences(ctx, containerID) - if err == preferences.ErrPreferencesNotFound { - record = preferences.GetDefaultPreferences(containerID) - } else if err != nil { - log.WithError(err).Warn("failure getting preferences record") - return nil, status.Error(codes.Internal, "") - } - - record.Locale = locale - - err = s.data.SaveUserPreferences(ctx, record) - if err != nil { - log.WithError(err).Warn("failure saving preferences record") - return nil, status.Error(codes.Internal, "") - } - - return &userpb.UpdatePreferencesResponse{ - Result: userpb.UpdatePreferencesResponse_OK, - }, nil -} - -func (s *identityServer) LoginToThirdPartyApp(ctx context.Context, req *userpb.LoginToThirdPartyAppRequest) (*userpb.LoginToThirdPartyAppResponse, error) { - log := s.log.WithField("method", "LoginToThirdPartyApp") - log = client.InjectLoggingMetadata(ctx, log) - - intentId, err := common.NewAccountFromPublicKeyBytes(req.IntentId.Value) - if err != nil { - log.WithError(err).Warn("intent id is invalid") - return nil, err - } - log = log.WithField("intent", intentId.PublicKey().ToBase58()) - - userAuthorityAccount, err := common.NewAccountFromProto(req.UserId) - if err != nil { - log.WithError(err).Warn("invalid authority account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("user", userAuthorityAccount.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, userAuthorityAccount, req, signature); err != nil { - return nil, err - } - - requestRecord, err := s.data.GetRequest(ctx, intentId.PublicKey().ToBase58()) - if err == paymentrequest.ErrPaymentRequestNotFound { - return &userpb.LoginToThirdPartyAppResponse{ - Result: userpb.LoginToThirdPartyAppResponse_REQUEST_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting request record") - return nil, status.Error(codes.Internal, "") - } - - if !requestRecord.HasLogin() { - return &userpb.LoginToThirdPartyAppResponse{ - Result: userpb.LoginToThirdPartyAppResponse_LOGIN_NOT_SUPPORTED, - }, nil - } - - if requestRecord.RequiresPayment() { - return &userpb.LoginToThirdPartyAppResponse{ - Result: userpb.LoginToThirdPartyAppResponse_PAYMENT_REQUIRED, - }, nil - } - - var isValidLoginAccount bool - accountInfoRecord, err := s.data.GetAccountInfoByAuthorityAddress(ctx, userAuthorityAccount.PublicKey().ToBase58()) - switch err { - case nil: - if accountInfoRecord.AccountType == commonpb.AccountType_RELATIONSHIP && *accountInfoRecord.RelationshipTo == *requestRecord.Domain { - isValidLoginAccount = true - } - case account.ErrAccountInfoNotFound: - default: - log.WithError(err).Warn("failure getting account info record") - return nil, status.Error(codes.Internal, "") - } - if !isValidLoginAccount { - return &userpb.LoginToThirdPartyAppResponse{ - Result: userpb.LoginToThirdPartyAppResponse_INVALID_ACCOUNT, - }, nil - } - - intentLock := s.intentLocks.Get(intentId.PublicKey().ToBytes()) - intentLock.Lock() - defer intentLock.Unlock() - - existingIntentRecord, err := s.data.GetIntent(ctx, intentId.PublicKey().ToBase58()) - switch err { - case nil: - if accountInfoRecord.OwnerAccount == existingIntentRecord.InitiatorOwnerAccount { - return &userpb.LoginToThirdPartyAppResponse{ - Result: userpb.LoginToThirdPartyAppResponse_OK, - }, nil - } - - return &userpb.LoginToThirdPartyAppResponse{ - Result: userpb.LoginToThirdPartyAppResponse_DIFFERENT_LOGIN_EXISTS, - }, nil - case intent.ErrIntentNotFound: - default: - log.WithError(err).Warn("failure checking for existing intent record") - return nil, status.Error(codes.Internal, "") - } - - intentRecord := &intent.Record{ - IntentId: intentId.PublicKey().ToBase58(), - IntentType: intent.Login, - LoginMetadata: &intent.LoginMetadata{ - App: *requestRecord.Domain, - UserId: accountInfoRecord.AuthorityAccount, - }, - InitiatorOwnerAccount: accountInfoRecord.OwnerAccount, - State: intent.StateConfirmed, - CreatedAt: time.Now(), - } - - err = s.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { - // todo: Ideally need a call with put semantics or proper distributed locks. - // Should be fine for now given the path uniquely handles the raw login - // case and everything happens in SubmitIntent. - err := s.data.SaveIntent(ctx, intentRecord) - if err != nil { - log.WithError(err).Warn("failure saving intent record") - return err - } - - err = s.markWebhookAsPending(ctx, intentRecord.IntentId) - if err != nil { - log.WithError(err).Warn("failure marking webhook as pending") - return err - } - - _, err = s.messagingClient.InternallyCreateMessage(ctx, intentId, &messagingpb.Message{ - Kind: &messagingpb.Message_IntentSubmitted{ - IntentSubmitted: &messagingpb.IntentSubmitted{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - // Metadata is hidden, since the details of who logged in should - // be gated behind an authenticated RPC - Metadata: nil, - }, - }, - }) - if err != nil { - log.WithError(err).Warn("failure creating intent submitted message") - return err - } - - return nil - }) - if err != nil { - return nil, status.Error(codes.Internal, "") - } - - return &userpb.LoginToThirdPartyAppResponse{ - Result: userpb.LoginToThirdPartyAppResponse_OK, - }, nil -} - -func (s *identityServer) GetLoginForThirdPartyApp(ctx context.Context, req *userpb.GetLoginForThirdPartyAppRequest) (*userpb.GetLoginForThirdPartyAppResponse, error) { - log := s.log.WithField("method", "GetLoginForThirdPartyApp") - log = client.InjectLoggingMetadata(ctx, log) - - intentId, err := common.NewAccountFromPublicKeyBytes(req.IntentId.Value) - if err != nil { - log.WithError(err).Warn("intent id is invalid") - return nil, err - } - log = log.WithField("intent", intentId.PublicKey().ToBase58()) - - requestRecord, err := s.data.GetRequest(ctx, intentId.PublicKey().ToBase58()) - if err == paymentrequest.ErrPaymentRequestNotFound { - return &userpb.GetLoginForThirdPartyAppResponse{ - Result: userpb.GetLoginForThirdPartyAppResponse_REQUEST_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting request record") - return nil, status.Error(codes.Internal, "") - } - - if !requestRecord.HasLogin() { - return &userpb.GetLoginForThirdPartyAppResponse{ - Result: userpb.GetLoginForThirdPartyAppResponse_LOGIN_NOT_SUPPORTED, - }, nil - } - - verifier, err := common.NewAccountFromProto(req.Verifier) - if err != nil { - log.WithError(err).Warn("invalid verifier") - return nil, status.Error(codes.Internal, "") - } - - // todo: Promote a generic utility to the auth package? - isVerified, err := s.domainVerifier(ctx, verifier, *requestRecord.Domain) - if err != nil { - log.WithError(err).Warn("failure verifying domain ownership") - return nil, status.Errorf(codes.Unauthenticated, "error veryfing domain ownership: %s", err.Error()) - } else if !isVerified { - return nil, status.Errorf(codes.Unauthenticated, "%s does not own the domain for the login", verifier.PublicKey().ToBase58()) - } - - intentRecord, err := s.data.GetIntent(ctx, intentId.PublicKey().ToBase58()) - if err == intent.ErrIntentNotFound { - return &userpb.GetLoginForThirdPartyAppResponse{ - Result: userpb.GetLoginForThirdPartyAppResponse_NO_USER_LOGGED_IN, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting intent record") - return nil, status.Error(codes.Internal, "") - } - - accountInfoRecord, err := s.data.GetRelationshipAccountInfoByOwnerAddress(ctx, intentRecord.InitiatorOwnerAccount, *requestRecord.Domain) - switch err { - case nil: - userId, err := common.NewAccountFromPublicKeyString(accountInfoRecord.AuthorityAccount) - if err != nil { - log.WithError(err).Warn("invalid authority account") - return nil, status.Error(codes.Internal, "") - } - - return &userpb.GetLoginForThirdPartyAppResponse{ - Result: userpb.GetLoginForThirdPartyAppResponse_OK, - UserId: userId.ToProto(), - }, nil - case account.ErrAccountInfoNotFound: - // The client opted to not establish a relationship, so there's no login - return &userpb.GetLoginForThirdPartyAppResponse{ - Result: userpb.GetLoginForThirdPartyAppResponse_NO_USER_LOGGED_IN, - }, nil - default: - log.WithError(err).Warn("failure getting relationship account info record") - return nil, status.Error(codes.Internal, "") - } -} - -func (s *identityServer) GetTwitterUser(ctx context.Context, req *userpb.GetTwitterUserRequest) (*userpb.GetTwitterUserResponse, error) { - log := s.log.WithField("method", "GetTwitterUser") - log = client.InjectLoggingMetadata(ctx, log) - - var record *twitter.Record - var err error - switch typed := req.Query.(type) { - case *userpb.GetTwitterUserRequest_Username: - log = log.WithField("username", typed.Username) - record, err = s.data.GetTwitterUserByUsername(ctx, typed.Username) - case *userpb.GetTwitterUserRequest_TipAddress: - log = log.WithField("tip_address", base58.Encode(typed.TipAddress.Value)) - record, err = s.data.GetTwitterUserByTipAddress(ctx, base58.Encode(typed.TipAddress.Value)) - default: - return nil, status.Error(codes.InvalidArgument, "req.query must be set") - } - - switch err { - case nil: - tipAddress, err := common.NewAccountFromPublicKeyString(record.TipAddress) - if err != nil { - log.WithError(err).Warn("tip address is invalid") - return nil, status.Error(codes.Internal, "") - } - - return &userpb.GetTwitterUserResponse{ - Result: userpb.GetTwitterUserResponse_OK, - TwitterUser: &userpb.TwitterUser{ - TipAddress: tipAddress.ToProto(), - Username: record.Username, - Name: record.Name, - ProfilePicUrl: record.ProfilePicUrl, - VerifiedType: record.VerifiedType, - FollowerCount: record.FollowerCount, - }, - }, nil - case twitter.ErrUserNotFound: - return &userpb.GetTwitterUserResponse{ - Result: userpb.GetTwitterUserResponse_NOT_FOUND, - }, nil - default: - log.WithError(err).Warn("failure getting twitter user info") - return nil, status.Error(codes.Internal, "") - } - -} - -func (s *identityServer) markWebhookAsPending(ctx context.Context, id string) error { - webhookRecord, err := s.data.GetWebhook(ctx, id) - if err == webhook.ErrNotFound { - return nil - } else if err != nil { - return err - } - - if webhookRecord.State != webhook.StateUnknown { - return nil - } - - webhookRecord.NextAttemptAt = pointer.Time(time.Now()) - webhookRecord.State = webhook.StatePending - return s.data.UpdateWebhook(ctx, webhookRecord) -} diff --git a/pkg/code/server/grpc/user/server_test.go b/pkg/code/server/grpc/user/server_test.go deleted file mode 100644 index 328fdaef..00000000 --- a/pkg/code/server/grpc/user/server_test.go +++ /dev/null @@ -1,1698 +0,0 @@ -package user - -import ( - "context" - "crypto/ed25519" - "strings" - "testing" - "time" - - "github.com/golang/protobuf/proto" - "github.com/mr-tron/base58" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/text/language" - xrate "golang.org/x/time/rate" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" - phonepb "github.com/code-payments/code-protobuf-api/generated/go/phone/v1" - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - userpb "github.com/code-payments/code-protobuf-api/generated/go/user/v1" - - "github.com/code-payments/code-server/pkg/code/antispam" - "github.com/code-payments/code-server/pkg/code/auth" - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/paymentrequest" - "github.com/code-payments/code-server/pkg/code/data/phone" - "github.com/code-payments/code-server/pkg/code/data/preferences" - "github.com/code-payments/code-server/pkg/code/data/twitter" - "github.com/code-payments/code-server/pkg/code/data/user" - "github.com/code-payments/code-server/pkg/code/data/user/identity" - "github.com/code-payments/code-server/pkg/code/data/user/storage" - "github.com/code-payments/code-server/pkg/code/data/webhook" - "github.com/code-payments/code-server/pkg/code/server/grpc/messaging" - transaction_server "github.com/code-payments/code-server/pkg/code/server/grpc/transaction/v2" - currency_lib "github.com/code-payments/code-server/pkg/currency" - memory_device_verifier "github.com/code-payments/code-server/pkg/device/memory" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/rate" - timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" - "github.com/code-payments/code-server/pkg/testutil" -) - -type testEnv struct { - ctx context.Context - client userpb.IdentityClient - server *identityServer - data code_data.Provider -} - -func setup(t *testing.T) (env testEnv, cleanup func()) { - conn, serv, err := testutil.NewServer() - require.NoError(t, err) - - // Force iOS user agent to pass airdrop tests - iosConn, err := grpc.Dial(conn.Target(), grpc.WithInsecure(), grpc.WithUserAgent("Code/iOS/1.0.0")) - require.NoError(t, err) - - env.ctx = context.Background() - env.client = userpb.NewIdentityClient(iosConn) - env.data = code_data.NewTestDataProvider() - - antispamGuard := antispam.NewGuard(env.data, memory_device_verifier.NewMemoryDeviceVerifier(), nil) - - s := NewIdentityServer( - env.data, - auth.NewRPCSignatureVerifier(env.data), - antispamGuard, - messaging.NewMessagingClient(env.data), - withManualTestOverrides(&testOverrides{}), - ) - env.server = s.(*identityServer) - env.server.domainVerifier = mockDomainVerifier - env.server.limiter = newLimiter(func(r float64) rate.Limiter { - return rate.NewLocalRateLimiter(xrate.Limit(r)) - }, 100, 100) - - testutil.SetupRandomSubsidizer(t, env.data) - - serv.RegisterService(func(server *grpc.Server) { - userpb.RegisterIdentityServer(server, s) - }) - - cleanup, err = serv.Serve() - require.NoError(t, err) - return env, cleanup -} - -func TestLinkAccount_PhoneHappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - phoneNumber := "+12223334444" - - for i := 0; i < 3; i++ { - verificationCode := "123456" - require.NoError(t, env.data.SavePhoneLinkingToken(env.ctx, &phone.LinkingToken{ - PhoneNumber: phoneNumber, - Code: verificationCode, - MaxCheckCount: 5, - ExpiresAt: time.Now().Add(1 * time.Hour), - })) - - linkReq := &userpb.LinkAccountRequest{ - OwnerAccountId: ownerAccount.ToProto(), - Token: &userpb.LinkAccountRequest_Phone{ - Phone: &phonepb.PhoneLinkingToken{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: verificationCode, - }, - }, - }, - } - - reqBytes, err := proto.Marshal(linkReq) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - linkReq.Signature = &commonpb.Signature{ - Value: signature, - } - - linkResp, err := env.client.LinkAccount(env.ctx, linkReq) - require.NoError(t, err) - assert.Equal(t, userpb.LinkAccountResponse_OK, linkResp.Result) - assert.NotEqual(t, linkResp.User.Id.Value, linkResp.DataContainerId.Value) - assert.True(t, linkResp.GetPhone().IsLinked) - - user, err := env.data.GetUserByPhoneView(env.ctx, phoneNumber) - require.NoError(t, err) - assert.Equal(t, user.ID.Proto(), linkResp.User.Id) - - container, err := env.data.GetUserDataContainerByPhone(env.ctx, ownerAccount.PublicKey().ToBase58(), phoneNumber) - require.NoError(t, err) - assert.Equal(t, container.ID.Proto(), linkResp.DataContainerId) - - verification, err := env.data.GetLatestPhoneVerificationForNumber(env.ctx, phoneNumber) - require.NoError(t, err) - assert.Equal(t, phoneNumber, verification.PhoneNumber) - assert.Equal(t, ownerAccount.PublicKey().ToBase58(), verification.OwnerAccount) - - err = env.data.UsePhoneLinkingToken(env.ctx, phoneNumber, verificationCode) - assert.Equal(t, phone.ErrLinkingTokenNotFound, err) - - getUserReq := &userpb.GetUserRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: "+12223334444", - }, - }, - } - - reqBytes, err = proto.Marshal(getUserReq) - require.NoError(t, err) - - signature, err = ownerAccount.Sign(reqBytes) - require.NoError(t, err) - getUserReq.Signature = &commonpb.Signature{ - Value: signature, - } - - getUserResp, err := env.client.GetUser(env.ctx, getUserReq) - require.NoError(t, err) - - assert.Equal(t, userpb.GetUserResponse_OK, getUserResp.Result) - assert.Equal(t, linkResp.User.Id.Value, getUserResp.User.Id.Value) - assert.Equal(t, linkResp.DataContainerId.Value, getUserResp.DataContainerId.Value) - assert.True(t, getUserResp.GetPhone().IsLinked) - assert.False(t, getUserResp.EnableInternalFlags) - assert.Len(t, getUserResp.EligibleAirdrops, 1) - assert.Equal(t, transactionpb.AirdropType_GET_FIRST_KIN, getUserResp.EligibleAirdrops[0]) - - unlinkReq := &userpb.UnlinkAccountRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.UnlinkAccountRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: "+12223334444", - }, - }, - } - - reqBytes, err = proto.Marshal(unlinkReq) - require.NoError(t, err) - - signature, err = ownerAccount.Sign(reqBytes) - require.NoError(t, err) - unlinkReq.Signature = &commonpb.Signature{ - Value: signature, - } - - unlinkResp, err := env.client.UnlinkAccount(env.ctx, unlinkReq) - require.NoError(t, err) - assert.Equal(t, userpb.UnlinkAccountResponse_OK, unlinkResp.Result) - - getUserResp, err = env.client.GetUser(env.ctx, getUserReq) - require.NoError(t, err) - assert.False(t, getUserResp.GetPhone().IsLinked) - } -} - -func TestLinkAccount_UserAlreadyExists(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - phoneNumber := "+12223334444" - verificationCode := "123456" - require.NoError(t, env.data.SavePhoneLinkingToken(env.ctx, &phone.LinkingToken{ - PhoneNumber: phoneNumber, - Code: verificationCode, - MaxCheckCount: 5, - ExpiresAt: time.Now().Add(1 * time.Hour), - })) - - userRecord := &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUser(env.ctx, userRecord)) - - containerRecord := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUserDataContainer(env.ctx, containerRecord)) - - linkReq := &userpb.LinkAccountRequest{ - OwnerAccountId: ownerAccount.ToProto(), - Token: &userpb.LinkAccountRequest_Phone{ - Phone: &phonepb.PhoneLinkingToken{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: verificationCode, - }, - }, - }, - } - - reqBytes, err := proto.Marshal(linkReq) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - linkReq.Signature = &commonpb.Signature{ - Value: signature, - } - - linkResp, err := env.client.LinkAccount(env.ctx, linkReq) - require.NoError(t, err) - assert.Equal(t, userpb.LinkAccountResponse_OK, linkResp.Result) - assert.Equal(t, userRecord.ID.Proto(), linkResp.User.Id) - assert.Equal(t, containerRecord.ID.Proto(), linkResp.DataContainerId) -} - -func TestLinkAccount_InvalidToken(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - phoneNumber := "+12223334444" - - req := &userpb.LinkAccountRequest{ - OwnerAccountId: ownerAccount.ToProto(), - Token: &userpb.LinkAccountRequest_Phone{ - Phone: &phonepb.PhoneLinkingToken{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - Code: &phonepb.VerificationCode{ - Value: "123456", - }, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.LinkAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, resp.Result, userpb.LinkAccountResponse_INVALID_TOKEN) - - require.NoError(t, env.data.SavePhoneLinkingToken(env.ctx, &phone.LinkingToken{ - PhoneNumber: phoneNumber, - Code: "999999", - MaxCheckCount: 5, - ExpiresAt: time.Now().Add(1 * time.Hour), - })) - - resp, err = env.client.LinkAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, resp.Result, userpb.LinkAccountResponse_INVALID_TOKEN) -} - -func TestUnlinkAccount_PhoneNeverAssociated(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - validPhoneNumber := "+12223334444" - invalidPhoneNumber := "+18005550000" - - userRecord := &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &validPhoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUser(env.ctx, userRecord)) - - req := &userpb.UnlinkAccountRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.UnlinkAccountRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: validPhoneNumber, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.UnlinkAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.UnlinkAccountResponse_NEVER_ASSOCIATED, resp.Result) - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: validPhoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - - resp, err = env.client.UnlinkAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.UnlinkAccountResponse_OK, resp.Result) - - req = &userpb.UnlinkAccountRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.UnlinkAccountRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: invalidPhoneNumber, - }, - }, - } - - reqBytes, err = proto.Marshal(req) - require.NoError(t, err) - - signature, err = ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err = env.client.UnlinkAccount(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.UnlinkAccountResponse_NEVER_ASSOCIATED, resp.Result) -} - -func TestGetUser_NotFound(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - phoneNumber := "+12223334444" - - req := &userpb.GetUserRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.GetUser(env.ctx, req) - require.NoError(t, err) - - assert.Equal(t, userpb.GetUserResponse_NOT_FOUND, resp.Result) - assert.Nil(t, resp.User) - assert.Nil(t, resp.DataContainerId) - assert.Nil(t, resp.Metadata) -} - -func TestGetUser_UnlockedTimelockAccount(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - phoneNumber := "+12223334444" - - userRecord := &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUser(env.ctx, userRecord)) - - containerRecord := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUserDataContainer(env.ctx, containerRecord)) - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - - timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) - require.NoError(t, err) - timelockRecord := timelockAccounts.ToDBRecord() - require.NoError(t, env.data.SaveTimelock(env.ctx, timelockRecord)) - - accountInfoRecord := &account.Record{ - OwnerAccount: timelockRecord.VaultOwner, - AuthorityAccount: timelockRecord.VaultOwner, - TokenAccount: timelockRecord.VaultAddress, - MintAccount: timelockAccounts.Mint.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_PRIMARY, - } - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) - - req := &userpb.GetUserRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.GetUser(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetUserResponse_OK, resp.Result) - - timelockRecord.VaultState = timelock_token.StateUnlocked - timelockRecord.Block += 1 - require.NoError(t, env.data.SaveTimelock(env.ctx, timelockRecord)) - - resp, err = env.client.GetUser(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetUserResponse_UNLOCKED_TIMELOCK_ACCOUNT, resp.Result) - assert.Nil(t, resp.User) - assert.Nil(t, resp.DataContainerId) -} - -func TestGetUser_LinkStatus(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - phoneNumbers := []string{ - "+12223334444", - "+18005550000", - } - - for _, phoneNumber := range phoneNumbers { - userRecord := &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUser(env.ctx, userRecord)) - - containerRecord := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUserDataContainer(env.ctx, containerRecord)) - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - } - - for _, phoneNumber := range phoneNumbers { - req := &userpb.GetUserRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.GetUser(env.ctx, req) - require.NoError(t, err) - - assert.Equal(t, userpb.GetUserResponse_OK, resp.Result) - assert.True(t, resp.GetPhone().IsLinked) - } - - for _, phoneNumber := range phoneNumbers { - req := &userpb.GetUserRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - for _, isUnlinked := range []bool{true, false} { - require.NoError(t, env.data.SaveOwnerAccountPhoneSetting(env.ctx, phoneNumber, &phone.OwnerAccountSetting{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IsUnlinked: &isUnlinked, - CreatedAt: time.Now(), - LastUpdatedAt: time.Now(), - })) - - resp, err := env.client.GetUser(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetUserResponse_OK, resp.Result) - assert.Equal(t, !isUnlinked, resp.GetPhone().IsLinked) - } - } - - for _, phoneNumber := range phoneNumbers { - otherOwnerAccount := testutil.NewRandomAccount(t) - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: otherOwnerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now().Add(1 * time.Hour), - })) - } - - for _, phoneNumber := range phoneNumbers { - req := &userpb.GetUserRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.GetUser(env.ctx, req) - require.NoError(t, err) - - assert.Equal(t, userpb.GetUserResponse_OK, resp.Result) - assert.False(t, resp.GetPhone().IsLinked) - } -} - -func TestGetUser_FeatureFlags(t *testing.T) { - for _, isStaffUser := range []bool{true, false} { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - phoneNumber := "+12223334444" - userRecord := &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - IsStaffUser: isStaffUser, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUser(env.ctx, userRecord)) - - containerRecord := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUserDataContainer(env.ctx, containerRecord)) - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - - req := &userpb.GetUserRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.GetUser(env.ctx, req) - require.NoError(t, err) - - assert.Equal(t, userpb.GetUserResponse_OK, resp.Result) - assert.Equal(t, isStaffUser, resp.EnableInternalFlags) - } -} - -func TestGetUser_AirdropStatus(t *testing.T) { - for _, saveFirstKinAirdropIntent := range []bool{true, false} { - for _, useNewAirdropIntentId := range []bool{true, false} { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - - phoneNumber := "+12223334444" - userRecord := &identity.Record{ - ID: user.NewUserID(), - View: &user.View{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUser(env.ctx, userRecord)) - - containerRecord := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUserDataContainer(env.ctx, containerRecord)) - - require.NoError(t, env.data.SavePhoneVerification(env.ctx, &phone.Verification{ - PhoneNumber: phoneNumber, - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - CreatedAt: time.Now(), - LastVerifiedAt: time.Now(), - })) - - if saveFirstKinAirdropIntent { - intentId := transaction_server.GetOldAirdropIntentId(transaction_server.AirdropTypeGetFirstKin, ownerAccount.PublicKey().ToBase58()) - if useNewAirdropIntentId { - intentId = transaction_server.GetNewAirdropIntentId(transaction_server.AirdropTypeGetFirstKin, ownerAccount.PublicKey().ToBase58()) - } - - intentRecord := &intent.Record{ - IntentId: intentId, - IntentType: intent.SendPublicPayment, - - SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ - DestinationOwnerAccount: ownerAccount.PublicKey().ToBase58(), - DestinationTokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - Quantity: kin.ToQuarks(1), - - ExchangeCurrency: currency_lib.USD, - ExchangeRate: 1.0, - NativeAmount: 1.0, - UsdMarketValue: 1.0, - }, - - InitiatorOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - State: intent.StatePending, - } - require.NoError(t, env.data.SaveIntent(env.ctx, intentRecord)) - } - - req := &userpb.GetUserRequest{ - OwnerAccountId: ownerAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: phoneNumber, - }, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - signature, err := ownerAccount.Sign(reqBytes) - require.NoError(t, err) - req.Signature = &commonpb.Signature{ - Value: signature, - } - - resp, err := env.client.GetUser(env.ctx, req) - require.NoError(t, err) - - assert.Equal(t, userpb.GetUserResponse_OK, resp.Result) - if saveFirstKinAirdropIntent { - require.Empty(t, resp.EligibleAirdrops) - } else { - require.Len(t, resp.EligibleAirdrops, 1) - assert.Equal(t, transactionpb.AirdropType_GET_FIRST_KIN, resp.EligibleAirdrops[0]) - } - } - } -} - -func TestUpdatePreferences_Locale_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - containerId := generateNewDataContainer(t, env, ownerAccount) - - for _, expected := range []string{ - "fr", - "fr-CA", - "fr_CA", - "en", - "en-US", - "en_US", - "mni-Beng_IN", - } { - req := &userpb.UpdatePreferencesRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerId.Proto(), - Locale: &commonpb.Locale{ - Value: expected, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), - } - - resp, err := env.client.UpdatePreferences(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.UpdatePreferencesResponse_OK, resp.Result) - - preferencesRecord, err := env.data.GetUserPreferences(env.ctx, containerId) - require.NoError(t, err) - assert.Equal(t, strings.Replace(expected, "_", "-", -1), preferencesRecord.Locale.String()) - } -} - -func TestUpdatePreferences_Locale_InvalidLocale(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - containerId := generateNewDataContainer(t, env, ownerAccount) - - for _, invalidValue := range []string{ - "zz", - } { - req := &userpb.UpdatePreferencesRequest{ - OwnerAccountId: ownerAccount.ToProto(), - ContainerId: containerId.Proto(), - Locale: &commonpb.Locale{ - Value: invalidValue, - }, - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), - } - - resp, err := env.client.UpdatePreferences(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.UpdatePreferencesResponse_INVALID_LOCALE, resp.Result) - - _, err = env.data.GetUserPreferences(env.ctx, containerId) - assert.Equal(t, preferences.ErrPreferencesNotFound, err) - } -} - -func TestLoginToThirdPartyApp_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - relationshipAuthorityAccount := testutil.NewRandomAccount(t) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.LoginToThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - UserId: relationshipAuthorityAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(relationshipAuthorityAccount.PrivateKey().ToBytes(), reqBytes), - } - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - Domain: pointer.String("example.com"), - IsVerified: true, - })) - - require.NoError(t, env.data.CreateWebhook(env.ctx, &webhook.Record{ - WebhookId: intentId.PublicKey().ToBase58(), - Url: "example.com/webhook", - Type: webhook.TypeIntentSubmitted, - State: webhook.StateUnknown, - })) - - require.NoError(t, env.data.CreateAccountInfo(env.ctx, &account.Record{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - AuthorityAccount: relationshipAuthorityAccount.PublicKey().ToBase58(), - TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - - AccountType: commonpb.AccountType_RELATIONSHIP, - RelationshipTo: pointer.String("example.com"), - })) - - resp, err := env.client.LoginToThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.LoginToThirdPartyAppResponse_OK, resp.Result) - - intentRecord, err := env.data.GetIntent(env.ctx, intentId.PublicKey().ToBase58()) - require.NoError(t, err) - assert.Equal(t, intentId.PublicKey().ToBase58(), intentRecord.IntentId) - assert.Equal(t, intent.Login, intentRecord.IntentType) - require.NotNil(t, intentRecord.LoginMetadata) - assert.Equal(t, "example.com", intentRecord.LoginMetadata.App) - assert.Equal(t, relationshipAuthorityAccount.PublicKey().ToBase58(), intentRecord.LoginMetadata.UserId) - assert.Equal(t, ownerAccount.PublicKey().ToBase58(), intentRecord.InitiatorOwnerAccount) - assert.Equal(t, intent.StateConfirmed, intentRecord.State) - - webhookRecord, err := env.data.GetWebhook(env.ctx, intentId.PublicKey().ToBase58()) - require.NoError(t, err) - assert.True(t, webhookRecord.NextAttemptAt.Before(time.Now())) - assert.Equal(t, webhook.StatePending, webhookRecord.State) - - messageRecords, err := env.data.GetMessages(env.ctx, intentId.PublicKey().ToBase58()) - require.NoError(t, err) - require.Len(t, messageRecords, 1) - - var protoMessage messagingpb.Message - require.NoError(t, proto.Unmarshal(messageRecords[0].Message, &protoMessage)) - require.NotNil(t, protoMessage.GetIntentSubmitted()) - assert.Equal(t, intentId.PublicKey().ToBytes(), protoMessage.GetIntentSubmitted().IntentId.Value) - assert.Nil(t, protoMessage.GetIntentSubmitted().Metadata) - - resp, err = env.client.LoginToThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.LoginToThirdPartyAppResponse_OK, resp.Result) -} - -func TestLoginToThirdPartyApp_RequestNotFound(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - relationshipAuthorityAccount := testutil.NewRandomAccount(t) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.LoginToThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - UserId: relationshipAuthorityAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(relationshipAuthorityAccount.PrivateKey().ToBytes(), reqBytes), - } - - require.NoError(t, env.data.CreateAccountInfo(env.ctx, &account.Record{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - AuthorityAccount: relationshipAuthorityAccount.PublicKey().ToBase58(), - TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - - AccountType: commonpb.AccountType_RELATIONSHIP, - RelationshipTo: pointer.String("example.com"), - })) - - resp, err := env.client.LoginToThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.LoginToThirdPartyAppResponse_REQUEST_NOT_FOUND, resp.Result) - - _, err = env.data.GetIntent(env.ctx, intentId.PublicKey().ToBase58()) - assert.Equal(t, intent.ErrIntentNotFound, err) -} - -func TestLoginToThirdPartyApp_MultipleUsers(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - relationshipAuthorityAccount := testutil.NewRandomAccount(t) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.LoginToThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - UserId: relationshipAuthorityAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(relationshipAuthorityAccount.PrivateKey().ToBytes(), reqBytes), - } - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - Domain: pointer.String("example.com"), - IsVerified: true, - })) - - require.NoError(t, env.data.SaveIntent(env.ctx, &intent.Record{ - IntentId: intentId.PublicKey().ToBase58(), - IntentType: intent.Login, - - LoginMetadata: &intent.LoginMetadata{ - App: "example.com", - UserId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - }, - - InitiatorOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - State: intent.StateConfirmed, - })) - - require.NoError(t, env.data.CreateAccountInfo(env.ctx, &account.Record{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - AuthorityAccount: relationshipAuthorityAccount.PublicKey().ToBase58(), - TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - - AccountType: commonpb.AccountType_RELATIONSHIP, - RelationshipTo: pointer.String("example.com"), - })) - - resp, err := env.client.LoginToThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.LoginToThirdPartyAppResponse_DIFFERENT_LOGIN_EXISTS, resp.Result) - - intentRecord, err := env.data.GetIntent(env.ctx, intentId.PublicKey().ToBase58()) - require.NoError(t, err) - assert.NotEqual(t, intentRecord.InitiatorOwnerAccount, ownerAccount.PublicKey().ToBase58()) - assert.NotEqual(t, intentRecord.LoginMetadata.UserId, relationshipAuthorityAccount.PublicKey().ToBase58()) -} - -func TestLoginToThirdPartyApp_InvalidAccount(t *testing.T) { - for _, accountType := range []commonpb.AccountType{ - commonpb.AccountType_RELATIONSHIP, - commonpb.AccountType_PRIMARY, - commonpb.AccountType_BUCKET_100_KIN, - commonpb.AccountType_TEMPORARY_INCOMING, - commonpb.AccountType_REMOTE_SEND_GIFT_CARD, - } { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - authorityAccount := testutil.NewRandomAccount(t) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.LoginToThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - UserId: authorityAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(authorityAccount.PrivateKey().ToBytes(), reqBytes), - } - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - Domain: pointer.String("app1.com"), - IsVerified: true, - })) - - accountInfoRecord := &account.Record{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - AuthorityAccount: authorityAccount.PublicKey().ToBase58(), - TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - AccountType: accountType, - } - if accountType == commonpb.AccountType_PRIMARY || accountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { - accountInfoRecord.OwnerAccount = authorityAccount.PublicKey().ToBase58() - ownerAccount = authorityAccount - } - if accountType == commonpb.AccountType_RELATIONSHIP { - accountInfoRecord.RelationshipTo = pointer.String("app2.com") - } - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) - - resp, err := env.client.LoginToThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.LoginToThirdPartyAppResponse_INVALID_ACCOUNT, resp.Result) - - _, err = env.data.GetIntent(env.ctx, intentId.PublicKey().ToBase58()) - assert.Equal(t, intent.ErrIntentNotFound, err) - } -} - -func TestLoginToThirdPartyApp_LoginNotSupported(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - relationshipAuthorityAccount := testutil.NewRandomAccount(t) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.LoginToThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - UserId: relationshipAuthorityAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(relationshipAuthorityAccount.PrivateKey().ToBytes(), reqBytes), - } - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - DestinationTokenAccount: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - ExchangeCurrency: pointer.String(string(currency_lib.USD)), - NativeAmount: pointer.Float64(1.00), - })) - - require.NoError(t, env.data.CreateAccountInfo(env.ctx, &account.Record{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - AuthorityAccount: relationshipAuthorityAccount.PublicKey().ToBase58(), - TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - - AccountType: commonpb.AccountType_RELATIONSHIP, - RelationshipTo: pointer.String("example.com"), - })) - - resp, err := env.client.LoginToThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.LoginToThirdPartyAppResponse_LOGIN_NOT_SUPPORTED, resp.Result) - - _, err = env.data.GetIntent(env.ctx, intentId.PublicKey().ToBase58()) - assert.Equal(t, intent.ErrIntentNotFound, err) -} - -func TestLoginToThirdPartyApp_PaymentRequired(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - ownerAccount := testutil.NewRandomAccount(t) - relationshipAuthorityAccount := testutil.NewRandomAccount(t) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.LoginToThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - UserId: relationshipAuthorityAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(relationshipAuthorityAccount.PrivateKey().ToBytes(), reqBytes), - } - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - DestinationTokenAccount: pointer.String(testutil.NewRandomAccount(t).PublicKey().ToBase58()), - ExchangeCurrency: pointer.String(string(currency_lib.USD)), - NativeAmount: pointer.Float64(1.00), - Domain: pointer.String("example.com"), - IsVerified: true, - })) - - require.NoError(t, env.data.CreateAccountInfo(env.ctx, &account.Record{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - AuthorityAccount: relationshipAuthorityAccount.PublicKey().ToBase58(), - TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - - AccountType: commonpb.AccountType_RELATIONSHIP, - RelationshipTo: pointer.String("example.com"), - })) - - resp, err := env.client.LoginToThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.LoginToThirdPartyAppResponse_PAYMENT_REQUIRED, resp.Result) - - _, err = env.data.GetIntent(env.ctx, intentId.PublicKey().ToBase58()) - assert.Equal(t, intent.ErrIntentNotFound, err) -} - -func TestGetLoginForThirdPartyApp_HappyPath(t *testing.T) { - paymentRequestRecord := &paymentrequest.Record{ - DestinationTokenAccount: pointer.String(testutil.NewRandomAccount(t).PrivateKey().ToBase58()), - ExchangeCurrency: pointer.String(string(currency_lib.USD)), - NativeAmount: pointer.Float64(1.0), - Domain: pointer.String("example.com"), - IsVerified: true, - } - loginRequestRecord := &paymentrequest.Record{ - Domain: pointer.String("example.com"), - IsVerified: true, - } - - paymentIntentRecord := &intent.Record{ - IntentType: intent.SendPrivatePayment, - - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - ExchangeCurrency: currency_lib.Code(*paymentRequestRecord.ExchangeCurrency), - NativeAmount: *paymentRequestRecord.NativeAmount, - ExchangeRate: 0.1, - Quantity: kin.ToQuarks(10), - UsdMarketValue: *paymentRequestRecord.NativeAmount, - - DestinationTokenAccount: *paymentRequestRecord.DestinationTokenAccount, - - IsMicroPayment: true, - IsWithdrawal: true, - }, - - State: intent.StatePending, - } - - loginIntentRecord := &intent.Record{ - IntentType: intent.Login, - - LoginMetadata: &intent.LoginMetadata{ - App: "example.com", - UserId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - }, - - State: intent.StateConfirmed, - } - - for _, tc := range []struct { - requestRecord *paymentrequest.Record - intentRecord *intent.Record - }{ - {paymentRequestRecord, paymentIntentRecord}, - {loginRequestRecord, loginIntentRecord}, - } { - - env, cleanup := setup(t) - defer cleanup() - - verifierAccount, err := common.NewAccountFromPrivateKeyString("dr2MUzL4NCS45qyp16vDXiSdHqqdg2DF79xKaYMB1vzVtDDjPvyQ8xTH4VsTWXSDP3NFzsdCV6gEoChKftzwLno") - require.NoError(t, err) - - intentId := testutil.NewRandomAccount(t) - - ownerAccount := testutil.NewRandomAccount(t) - relationshipAuthorityAccount := testutil.NewRandomAccount(t) - - require.NoError(t, env.data.CreateAccountInfo(env.ctx, &account.Record{ - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - AuthorityAccount: relationshipAuthorityAccount.PublicKey().ToBase58(), - TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - - AccountType: commonpb.AccountType_RELATIONSHIP, - RelationshipTo: pointer.String("example.com"), - })) - - req := &userpb.GetLoginForThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - Verifier: verifierAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(verifierAccount.PrivateKey().ToBytes(), reqBytes), - } - - tc.requestRecord.Intent = intentId.PublicKey().ToBase58() - require.NoError(t, env.data.CreateRequest(env.ctx, tc.requestRecord)) - - resp, err := env.client.GetLoginForThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetLoginForThirdPartyAppResponse_NO_USER_LOGGED_IN, resp.Result) - assert.Nil(t, resp.UserId) - - tc.intentRecord.IntentId = intentId.PublicKey().ToBase58() - tc.intentRecord.InitiatorOwnerAccount = ownerAccount.PublicKey().ToBase58() - require.NoError(t, env.data.SaveIntent(env.ctx, tc.intentRecord)) - - resp, err = env.client.GetLoginForThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetLoginForThirdPartyAppResponse_OK, resp.Result) - require.NotNil(t, resp.UserId) - assert.Equal(t, relationshipAuthorityAccount.PublicKey().ToBytes(), resp.UserId.Value) - } -} - -func TestGetLoginForThirdPartyApp_RequestNotFound(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - verifierAccount, err := common.NewAccountFromPrivateKeyString("dr2MUzL4NCS45qyp16vDXiSdHqqdg2DF79xKaYMB1vzVtDDjPvyQ8xTH4VsTWXSDP3NFzsdCV6gEoChKftzwLno") - require.NoError(t, err) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.GetLoginForThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - Verifier: verifierAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(verifierAccount.PrivateKey().ToBytes(), reqBytes), - } - - resp, err := env.client.GetLoginForThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetLoginForThirdPartyAppResponse_REQUEST_NOT_FOUND, resp.Result) - assert.Nil(t, resp.UserId) -} - -func TestGetLoginForThirdPartyApp_LoginNotSupported(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - verifierAccount, err := common.NewAccountFromPrivateKeyString("dr2MUzL4NCS45qyp16vDXiSdHqqdg2DF79xKaYMB1vzVtDDjPvyQ8xTH4VsTWXSDP3NFzsdCV6gEoChKftzwLno") - require.NoError(t, err) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.GetLoginForThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - Verifier: verifierAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(verifierAccount.PrivateKey().ToBytes(), reqBytes), - } - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - DestinationTokenAccount: pointer.String(testutil.NewRandomAccount(t).PrivateKey().ToBase58()), - ExchangeCurrency: pointer.String(string(currency_lib.USD)), - NativeAmount: pointer.Float64(1.0), - Domain: pointer.String("example.com"), - IsVerified: false, - })) - - resp, err := env.client.GetLoginForThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetLoginForThirdPartyAppResponse_LOGIN_NOT_SUPPORTED, resp.Result) - assert.Nil(t, resp.UserId) -} - -func TestGetLoginForThirdPartyApp_RelationshipNotEstablished(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - verifierAccount, err := common.NewAccountFromPrivateKeyString("dr2MUzL4NCS45qyp16vDXiSdHqqdg2DF79xKaYMB1vzVtDDjPvyQ8xTH4VsTWXSDP3NFzsdCV6gEoChKftzwLno") - require.NoError(t, err) - - intentId := testutil.NewRandomAccount(t) - - req := &userpb.GetLoginForThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - Verifier: verifierAccount.ToProto(), - } - - reqBytes, err := proto.Marshal(req) - require.NoError(t, err) - - req.Signature = &commonpb.Signature{ - Value: ed25519.Sign(verifierAccount.PrivateKey().ToBytes(), reqBytes), - } - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - DestinationTokenAccount: pointer.String(testutil.NewRandomAccount(t).PrivateKey().ToBase58()), - ExchangeCurrency: pointer.String(string(currency_lib.USD)), - NativeAmount: pointer.Float64(1.0), - Domain: pointer.String("example.com"), - IsVerified: true, - })) - - require.NoError(t, env.data.SaveIntent(env.ctx, &intent.Record{ - IntentId: intentId.PublicKey().ToBase58(), - IntentType: intent.SendPrivatePayment, - - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ - ExchangeCurrency: currency_lib.USD, - NativeAmount: 1.0, - ExchangeRate: 0.1, - Quantity: kin.ToQuarks(10), - UsdMarketValue: 1.0, - - DestinationTokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - IsMicroPayment: true, - IsWithdrawal: true, - }, - - InitiatorOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - State: intent.StatePending, - })) - - resp, err := env.client.GetLoginForThirdPartyApp(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetLoginForThirdPartyAppResponse_NO_USER_LOGGED_IN, resp.Result) - assert.Nil(t, resp.UserId) -} - -func TestGetTwitterUser_ByUsername_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - req := &userpb.GetTwitterUserRequest{ - Query: &userpb.GetTwitterUserRequest_Username{ - Username: "jeffyanta", - }, - } - resp, err := env.client.GetTwitterUser(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetTwitterUserResponse_NOT_FOUND, resp.Result) - assert.Nil(t, resp.TwitterUser) - - record := &twitter.Record{ - Username: req.GetUsername(), - Name: "Jeff", - ProfilePicUrl: "https://pbs.twimg.com/profile_images/1728595562285441024/GM-aLyh__normal.jpg", - VerifiedType: userpb.TwitterUser_BLUE, - FollowerCount: 200, - TipAddress: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - } - require.NoError(t, env.data.SaveTwitterUser(env.ctx, record)) - - resp, err = env.client.GetTwitterUser(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetTwitterUserResponse_OK, resp.Result) - assert.Equal(t, record.TipAddress, base58.Encode(resp.TwitterUser.TipAddress.Value)) - assert.Equal(t, record.Username, resp.TwitterUser.Username) - assert.Equal(t, record.Name, resp.TwitterUser.Name) - assert.Equal(t, record.ProfilePicUrl, resp.TwitterUser.ProfilePicUrl) - assert.Equal(t, record.VerifiedType, resp.TwitterUser.VerifiedType) - assert.Equal(t, record.FollowerCount, resp.TwitterUser.FollowerCount) -} - -func TestGetTwitterUser_ByTipAddress_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - tipAddress := testutil.NewRandomAccount(t) - - req := &userpb.GetTwitterUserRequest{ - Query: &userpb.GetTwitterUserRequest_TipAddress{ - TipAddress: tipAddress.ToProto(), - }, - } - resp, err := env.client.GetTwitterUser(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetTwitterUserResponse_NOT_FOUND, resp.Result) - assert.Nil(t, resp.TwitterUser) - - record := &twitter.Record{ - Username: "jeffyanta", - Name: "Jeff", - ProfilePicUrl: "https://pbs.twimg.com/profile_images/1728595562285441024/GM-aLyh__normal.jpg", - VerifiedType: userpb.TwitterUser_BLUE, - FollowerCount: 200, - TipAddress: tipAddress.PublicKey().ToBase58(), - } - require.NoError(t, env.data.SaveTwitterUser(env.ctx, record)) - - resp, err = env.client.GetTwitterUser(env.ctx, req) - require.NoError(t, err) - assert.Equal(t, userpb.GetTwitterUserResponse_OK, resp.Result) - assert.Equal(t, record.TipAddress, base58.Encode(resp.TwitterUser.TipAddress.Value)) - assert.Equal(t, record.Username, resp.TwitterUser.Username) - assert.Equal(t, record.Name, resp.TwitterUser.Name) - assert.Equal(t, record.ProfilePicUrl, resp.TwitterUser.ProfilePicUrl) - assert.Equal(t, record.VerifiedType, resp.TwitterUser.VerifiedType) - assert.Equal(t, record.FollowerCount, resp.TwitterUser.FollowerCount) -} - -func TestUnauthenticatedRPC(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - validAccount := testutil.NewRandomAccount(t) - maliciousAccount := testutil.NewRandomAccount(t) - - intentId := testutil.NewRandomAccount(t) - containerId := generateNewDataContainer(t, env, validAccount) - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - Domain: pointer.String("example.com"), - IsVerified: true, - })) - - linkReq := &userpb.LinkAccountRequest{ - OwnerAccountId: validAccount.ToProto(), - Token: &userpb.LinkAccountRequest_Phone{ - Phone: &phonepb.PhoneLinkingToken{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: "+12223334444", - }, - Code: &phonepb.VerificationCode{ - Value: "123456", - }, - }, - }, - } - - reqBytes, err := proto.Marshal(linkReq) - require.NoError(t, err) - - signature, err := maliciousAccount.Sign(reqBytes) - require.NoError(t, err) - linkReq.Signature = &commonpb.Signature{ - Value: signature, - } - - getUserReq := &userpb.GetUserRequest{ - OwnerAccountId: validAccount.ToProto(), - IdentifyingFeature: &userpb.GetUserRequest_PhoneNumber{ - PhoneNumber: &commonpb.PhoneNumber{ - Value: "+12223334444", - }, - }, - } - - reqBytes, err = proto.Marshal(getUserReq) - require.NoError(t, err) - - signature, err = maliciousAccount.Sign(reqBytes) - require.NoError(t, err) - getUserReq.Signature = &commonpb.Signature{ - Value: signature, - } - - updatePreferencesReq := &userpb.UpdatePreferencesRequest{ - OwnerAccountId: validAccount.ToProto(), - ContainerId: containerId.Proto(), - Locale: &commonpb.Locale{ - Value: language.CanadianFrench.String(), - }, - } - - reqBytes, err = proto.Marshal(updatePreferencesReq) - require.NoError(t, err) - - updatePreferencesReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(maliciousAccount.PrivateKey().ToBytes(), reqBytes), - } - - loginReq := &userpb.LoginToThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - UserId: validAccount.ToProto(), - } - - reqBytes, err = proto.Marshal(loginReq) - require.NoError(t, err) - - loginReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(maliciousAccount.PrivateKey().ToBytes(), reqBytes), - } - - getLoginReq := &userpb.GetLoginForThirdPartyAppRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.ToProto().Value, - }, - Verifier: testutil.NewRandomAccount(t).ToProto(), - } - - reqBytes, err = proto.Marshal(getLoginReq) - require.NoError(t, err) - - getLoginReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(maliciousAccount.PrivateKey().ToBytes(), reqBytes), - } - - _, err = env.client.LinkAccount(env.ctx, linkReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - _, err = env.client.GetUser(env.ctx, getUserReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - _, err = env.client.UpdatePreferences(env.ctx, updatePreferencesReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - _, err = env.client.LoginToThirdPartyApp(env.ctx, loginReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) - - _, err = env.client.GetLoginForThirdPartyApp(env.ctx, getLoginReq) - testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) -} - -func TestUnauthorizedDataAccess(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - validAccount := testutil.NewRandomAccount(t) - maliciousAccount := testutil.NewRandomAccount(t) - - intentId := testutil.NewRandomAccount(t) - containerId := generateNewDataContainer(t, env, validAccount) - - require.NoError(t, env.data.CreateRequest(env.ctx, &paymentrequest.Record{ - Intent: intentId.PublicKey().ToBase58(), - Domain: pointer.String("example.com"), - IsVerified: true, - })) - - updatePreferencesReq := &userpb.UpdatePreferencesRequest{ - OwnerAccountId: maliciousAccount.ToProto(), - ContainerId: containerId.Proto(), - Locale: &commonpb.Locale{ - Value: language.CanadianFrench.String(), - }, - } - - reqBytes, err := proto.Marshal(updatePreferencesReq) - require.NoError(t, err) - - updatePreferencesReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(maliciousAccount.PrivateKey().ToBytes(), reqBytes), - } - - _, err = env.client.UpdatePreferences(env.ctx, updatePreferencesReq) - testutil.AssertStatusErrorWithCode(t, err, codes.PermissionDenied) -} - -func generateNewDataContainer(t *testing.T, env testEnv, ownerAccount *common.Account) *user.DataContainerID { - phoneNumber := "+12223334444" - - container := &storage.Record{ - ID: user.NewDataContainerID(), - OwnerAccount: ownerAccount.PublicKey().ToBase58(), - IdentifyingFeatures: &user.IdentifyingFeatures{ - PhoneNumber: &phoneNumber, - }, - CreatedAt: time.Now(), - } - require.NoError(t, env.data.PutUserDataContainer(env.ctx, container)) - return container.ID -} - -func mockDomainVerifier(ctx context.Context, owner *common.Account, domain string) (bool, error) { - // Private key: dr2MUzL4NCS45qyp16vDXiSdHqqdg2DF79xKaYMB1vzVtDDjPvyQ8xTH4VsTWXSDP3NFzsdCV6gEoChKftzwLno - return owner.PublicKey().ToBase58() == "AiXmGd1DkRbVyfiLLNxC6EFF9ZidCdGpyVY9QFH966Bm", nil -} diff --git a/pkg/code/server/grpc/messaging/client.go b/pkg/code/server/messaging/client.go similarity index 100% rename from pkg/code/server/grpc/messaging/client.go rename to pkg/code/server/messaging/client.go diff --git a/pkg/code/server/grpc/messaging/config.go b/pkg/code/server/messaging/config.go similarity index 100% rename from pkg/code/server/grpc/messaging/config.go rename to pkg/code/server/messaging/config.go diff --git a/pkg/code/server/grpc/messaging/error.go b/pkg/code/server/messaging/error.go similarity index 100% rename from pkg/code/server/grpc/messaging/error.go rename to pkg/code/server/messaging/error.go diff --git a/pkg/code/server/grpc/messaging/internal.go b/pkg/code/server/messaging/internal.go similarity index 100% rename from pkg/code/server/grpc/messaging/internal.go rename to pkg/code/server/messaging/internal.go diff --git a/pkg/code/server/grpc/messaging/message_handler.go b/pkg/code/server/messaging/message_handler.go similarity index 77% rename from pkg/code/server/grpc/messaging/message_handler.go rename to pkg/code/server/messaging/message_handler.go index 080cf18d..94470b74 100644 --- a/pkg/code/server/grpc/messaging/message_handler.go +++ b/pkg/code/server/messaging/message_handler.go @@ -3,7 +3,6 @@ package messaging import ( "bytes" "context" - "math" "time" "github.com/mr-tron/base58" @@ -23,7 +22,6 @@ import ( "github.com/code-payments/code-server/pkg/code/limit" "github.com/code-payments/code-server/pkg/code/thirdparty" currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" ) @@ -140,21 +138,14 @@ func (h *RequestToReceiveBillMessageHandler) Validate(ctx context.Context, rende exchangeRate = &typed.Exact.ExchangeRate quarks = &typed.Exact.Quarks - if currency != currency_lib.KIN { - return newMessageValidationError("exact exchange data is reserved for kin only") - } - - if nativeAmount != math.Trunc(nativeAmount) { - return newMessageValidationError("native amount can't include fractional kin") - } - if *quarks%kin.QuarksPerKin != 0 { - return newMessageValidationError("quark amount can't include fractional kin") + if currency != common.CoreMintSymbol { + return newMessageValidationError("exact exchange data is reserved for core mint only") } case *messagingpb.RequestToReceiveBill_Partial: currency = currency_lib.Code(typed.Partial.Currency) nativeAmount = typed.Partial.NativeAmount - if currency == currency_lib.KIN { + if currency == common.CoreMintSymbol { return newMessageValidationError("partial exchange data is reserved for fiat currencies") } default: @@ -435,20 +426,12 @@ func (h *RequestToReceiveBillMessageHandler) validateDestinationAccount( case nil: switch accountInfoRecord.AccountType { case commonpb.AccountType_PRIMARY: - case commonpb.AccountType_RELATIONSHIP: - if !isVerified { - return newMessageValidationError("domain verification is required when using a relationship account") - } - - if *accountInfoRecord.RelationshipTo != asciiBaseDomain { - return newMessageValidationErrorf("relationship account %s is not associated with %s", accountToValidate.PublicKey().ToBase58(), asciiBaseDomain) - } default: return newMessageValidationErrorf("code account %s is not a deposit account", accountToValidate.PublicKey().ToBase58()) } case account.ErrAccountInfoNotFound: if !h.conf.disableBlockchainChecks.Get(ctx) { - err := validateExternalKinTokenAccountWithinMessage(ctx, h.data, accountToValidate) + err := validateExternalTokenAccountWithinMessage(ctx, h.data, accountToValidate) if err != nil { return err } @@ -512,145 +495,6 @@ func (h *CodeScannedMessageHandler) OnSuccess(ctx context.Context) error { return nil } -type RequestToLoginMessageHandler struct { - data code_data.Provider - rpcSignatureVerifier *auth.RPCSignatureVerifier - domainVerifier thirdparty.DomainVerifier - - recordAlreadyExists bool - recordToSave *paymentrequest.Record -} - -func NewRequestToLoginMessageHandler(data code_data.Provider, rpcSignatureVerifier *auth.RPCSignatureVerifier, domainVerifier thirdparty.DomainVerifier) MessageHandler { - return &RequestToLoginMessageHandler{ - data: data, - rpcSignatureVerifier: rpcSignatureVerifier, - domainVerifier: domainVerifier, - } -} - -func (h *RequestToLoginMessageHandler) Validate(ctx context.Context, rendezvous *common.Account, untypedMessage *messagingpb.Message) error { - typedMessage := untypedMessage.GetRequestToLogin() - if typedMessage == nil { - return errors.New("invalid message type") - } - - owner, err := common.NewAccountFromProto(typedMessage.Verifier) - if err != nil { - return err - } - - signature := typedMessage.Signature - typedMessage.Signature = nil - if err := h.rpcSignatureVerifier.Authenticate(ctx, owner, typedMessage, signature); err != nil { - return newMessageAuthenticationError("") - } - typedMessage.Signature = signature - - // - // Part 1: Validate the intent doesn't exist - // - - _, err = h.data.GetIntent(ctx, rendezvous.PublicKey().ToBase58()) - if err == nil { - return newMessageValidationError("client submitted intent") - } else if err != intent.ErrIntentNotFound { - return err - } - - // - // Part 2: Validate the request metadata - // - - asciiBaseDomain, err := thirdparty.GetAsciiBaseDomain(typedMessage.Domain.Value) - if err != nil { - return newMessageValidationErrorf("domain is invalid: %s", err.Error()) - } - - if !bytes.Equal(rendezvous.PublicKey().ToBytes(), typedMessage.RendezvousKey.Value) { - return newMessageValidationError("rendezvous key mismatch") - } - - existingRequestRecord, err := h.data.GetRequest(ctx, rendezvous.PublicKey().ToBase58()) - switch err { - case nil: - if existingRequestRecord.RequiresPayment() { - return newMessageValidationError("original request requires payment") - } - - if *existingRequestRecord.Domain != asciiBaseDomain { - return newMessageValidationError("domain mismatches original request") - } - - h.recordAlreadyExists = true - case paymentrequest.ErrPaymentRequestNotFound: - default: - return err - } - - // - // Part 3: Domain validation - // - - err = verifyThirdPartyDomain(ctx, h.domainVerifier, owner, typedMessage.Domain) - if err != nil { - return err - } - - // - // Part 4: Create the validated payment request DB record to store later, - // if it doesn't already exist - // - - if !h.recordAlreadyExists { - h.recordToSave = &paymentrequest.Record{ - Intent: rendezvous.PublicKey().ToBase58(), - - Domain: &asciiBaseDomain, - IsVerified: true, - - CreatedAt: time.Now(), - } - } - - return nil -} - -func (h *RequestToLoginMessageHandler) RequiresActiveStream() (bool, time.Duration) { - return false, 0 * time.Minute -} - -func (h *RequestToLoginMessageHandler) OnSuccess(ctx context.Context) error { - if h.recordAlreadyExists { - return nil - } - return h.data.CreateRequest(ctx, h.recordToSave) -} - -type ClientRejectedLoginMessageHandler struct { -} - -func NewClientRejectedLoginMessageHandler() MessageHandler { - return &ClientRejectedLoginMessageHandler{} -} - -func (h *ClientRejectedLoginMessageHandler) Validate(ctx context.Context, rendezvous *common.Account, untypedMessage *messagingpb.Message) error { - typedMessage := untypedMessage.GetClientRejectedLogin() - if typedMessage == nil { - return errors.New("invalid message type") - } - - return nil -} - -func (h *ClientRejectedLoginMessageHandler) RequiresActiveStream() (bool, time.Duration) { - return false, 0 * time.Minute -} - -func (h *ClientRejectedLoginMessageHandler) OnSuccess(ctx context.Context) error { - return nil -} - func validateExchangeDataWithinMessage(ctx context.Context, data code_data.Provider, proto *transactionpb.ExchangeData) error { isValid, message, err := exchange_rate_util.ValidateClientExchangeData(ctx, data, proto) if err != nil { @@ -661,8 +505,8 @@ func validateExchangeDataWithinMessage(ctx context.Context, data code_data.Provi return nil } -func validateExternalKinTokenAccountWithinMessage(ctx context.Context, data code_data.Provider, tokenAccount *common.Account) error { - isValid, message, err := common.ValidateExternalKinTokenAccount(ctx, data, tokenAccount) +func validateExternalTokenAccountWithinMessage(ctx context.Context, data code_data.Provider, tokenAccount *common.Account) error { + isValid, message, err := common.ValidateExternalTokenAccount(ctx, data, tokenAccount) if err != nil { return err } else if !isValid { diff --git a/pkg/code/server/grpc/messaging/server.go b/pkg/code/server/messaging/server.go similarity index 98% rename from pkg/code/server/grpc/messaging/server.go rename to pkg/code/server/messaging/server.go index 85308c09..aaec8bf2 100644 --- a/pkg/code/server/grpc/messaging/server.go +++ b/pkg/code/server/messaging/server.go @@ -679,12 +679,18 @@ func (s *server) SendMessage(ctx context.Context, req *messagingpb.SendMessageRe // case *messagingpb.Message_RequestToReceiveBill: + return nil, status.Error(codes.InvalidArgument, "payment requests require rewrite") + log = log.WithField("message_type", "request_to_receive_bill") messageHandler = NewRequestToReceiveBillMessageHandler(s.conf, s.data, s.rpcSignatureVerifier, s.domainVerifier) case *messagingpb.Message_ClientRejectedPayment: + return nil, status.Error(codes.InvalidArgument, "payment requests require rewrite") + log = log.WithField("message_type", "client_rejected_payment") messageHandler = NewClientRejectedPaymentMessageHandler() case *messagingpb.Message_CodeScanned: + return nil, status.Error(codes.InvalidArgument, "payment requests require rewrite") + log = log.WithField("message_type", "code_scanned") messageHandler = NewCodeScannedMessageHandler() case *messagingpb.Message_IntentSubmitted: @@ -692,17 +698,6 @@ func (s *server) SendMessage(ctx context.Context, req *messagingpb.SendMessageRe case *messagingpb.Message_WebhookCalled: return nil, status.Error(codes.InvalidArgument, "message.kind cannot be webhook_called") - // - // Section: Login - // - - case *messagingpb.Message_RequestToLogin: - log = log.WithField("message_type", "request_to_login") - messageHandler = NewRequestToLoginMessageHandler(s.data, s.rpcSignatureVerifier, s.domainVerifier) - case *messagingpb.Message_ClientRejectedLogin: - log = log.WithField("message_type", "client_rejected_login") - messageHandler = NewClientRejectedLoginMessageHandler() - // // Section: Airdrops // diff --git a/pkg/code/server/grpc/messaging/server_test.go b/pkg/code/server/messaging/server_test.go similarity index 83% rename from pkg/code/server/grpc/messaging/server_test.go rename to pkg/code/server/messaging/server_test.go index e6fc5d90..471889dc 100644 --- a/pkg/code/server/grpc/messaging/server_test.go +++ b/pkg/code/server/messaging/server_test.go @@ -224,6 +224,8 @@ func TestSendMessage_RequestToGrabBill_Validation(t *testing.T) { } +/* + func TestSendMessage_RequestToReceiveBill_KinValue_HappyPath(t *testing.T) { for _, tc := range []struct { usePrimary bool @@ -419,7 +421,7 @@ func TestSendMessage_RequestToReceiveBill_KinValue_Validation(t *testing.T) { sendMessageCall = env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{ disableDomainVerification: true, }) - sendMessageCall.assertInvalidMessageError(t, "kin exchange rate must be 1") + sendMessageCall.assertInvalidMessageError(t, "core mint exchange rate must be 1") env.server1.assertNoMessages(t, rendezvousKey) env.server1.assertRequestRecordNotSaved(t, rendezvousKey) @@ -437,7 +439,7 @@ func TestSendMessage_RequestToReceiveBill_KinValue_Validation(t *testing.T) { sendMessageCall = env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{ disableDomainVerification: true, }) - sendMessageCall.assertInvalidMessageError(t, "kin currency has a minimum amount of 2500.00") + sendMessageCall.assertInvalidMessageError(t, "usdc currency has a minimum amount of 0.05") env.server1.assertNoMessages(t, rendezvousKey) env.server1.assertRequestRecordNotSaved(t, rendezvousKey) @@ -446,25 +448,7 @@ func TestSendMessage_RequestToReceiveBill_KinValue_Validation(t *testing.T) { sendMessageCall = env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{ disableDomainVerification: true, }) - sendMessageCall.assertInvalidMessageError(t, "kin currency has a maximum amount of 250000.00") - env.server1.assertNoMessages(t, rendezvousKey) - env.server1.assertRequestRecordNotSaved(t, rendezvousKey) - - env.client1.resetConf() - env.client1.conf.simulateFractionalNativeAmount = true - sendMessageCall = env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{ - disableDomainVerification: true, - }) - sendMessageCall.assertInvalidMessageError(t, "native amount can't include fractional kin") - env.server1.assertNoMessages(t, rendezvousKey) - env.server1.assertRequestRecordNotSaved(t, rendezvousKey) - - env.client1.resetConf() - env.client1.conf.simulateFractionalQuarkAmount = true - sendMessageCall = env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{ - disableDomainVerification: true, - }) - sendMessageCall.assertInvalidMessageError(t, "quark amount can't include fractional kin") + sendMessageCall.assertInvalidMessageError(t, "usdc currency has a maximum amount of 5.00") env.server1.assertNoMessages(t, rendezvousKey) env.server1.assertRequestRecordNotSaved(t, rendezvousKey) @@ -473,7 +457,7 @@ func TestSendMessage_RequestToReceiveBill_KinValue_Validation(t *testing.T) { sendMessageCall = env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{ disableDomainVerification: true, }) - sendMessageCall.assertInvalidMessageError(t, "exact exchange data is reserved for kin only") + sendMessageCall.assertInvalidMessageError(t, "exact exchange data is reserved for core mint only") env.server1.assertNoMessages(t, rendezvousKey) env.server1.assertRequestRecordNotSaved(t, rendezvousKey) @@ -564,19 +548,6 @@ func TestSendMessage_RequestToReceiveBill_KinValue_Validation(t *testing.T) { sendMessageCall.assertInvalidMessageError(t, "rendezvous key mismatch") env.server1.assertNoMessages(t, rendezvousKey) env.server1.assertRequestRecordNotSaved(t, rendezvousKey) - - // - // Part 7: Upgrading request with a payment requirement - // - - env.client1.resetConf() - originalSendMessageRequest := env.client1.sendRequestToLoginMessage(t, rendezvousKey) - originalSendMessageRequest.requireSuccess(t) - env.client1.sendRequestToReceiveKinBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{}).assertInvalidMessageError(t, "original request doesn't require payment") - env.server1.assertLoginRequestRecordSaved(t, rendezvousKey, originalSendMessageRequest.req.Message.GetRequestToLogin()) - messages := env.client1.pollForMessages(t, rendezvousKey) - require.Len(t, messages, 1) - require.NotNil(t, messages[0].GetRequestToLogin()) } func TestSendMessage_RequestToReceiveBill_FiatValue_Validation(t *testing.T) { @@ -725,112 +696,8 @@ func TestSendMessage_RequestToReceiveBill_FiatValue_Validation(t *testing.T) { sendMessageCall.assertInvalidMessageError(t, "rendezvous key mismatch") env.server1.assertNoMessages(t, rendezvousKey) env.server1.assertRequestRecordNotSaved(t, rendezvousKey) - - // - // Part 7: Upgrading request with a payment requirement - // - - env.client1.resetConf() - originalSendMessageRequest := env.client1.sendRequestToLoginMessage(t, rendezvousKey) - originalSendMessageRequest.requireSuccess(t) - env.client1.sendRequestToReceiveFiatBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{}).assertInvalidMessageError(t, "original request doesn't require payment") - env.server1.assertLoginRequestRecordSaved(t, rendezvousKey, originalSendMessageRequest.req.Message.GetRequestToLogin()) - messages := env.client1.pollForMessages(t, rendezvousKey) - require.Len(t, messages, 1) - require.NotNil(t, messages[0].GetRequestToLogin()) -} - -func TestSendMessage_RequestToLogin_HappyPath(t *testing.T) { - env, cleanup := setup(t, false) - defer cleanup() - - rendezvousKey := testutil.NewRandomAccount(t) - sendMessageCall := env.client2.sendRequestToLoginMessage(t, rendezvousKey) - sendMessageCall.requireSuccess(t) - - records := env.server1.getMessages(t, rendezvousKey) - require.Len(t, records, 1) - assert.Equal(t, rendezvousKey.PublicKey().ToBase58(), records[0].Account) - assert.Equal(t, sendMessageCall.resp.MessageId.Value, records[0].MessageID[:]) - - var savedProtoMessage messagingpb.Message - require.NoError(t, proto.Unmarshal(records[0].Message, &savedProtoMessage)) - - assert.Equal(t, sendMessageCall.resp.MessageId.Value, savedProtoMessage.Id.Value) - require.NotNil(t, savedProtoMessage.GetRequestToLogin()) - assert.Equal(t, sendMessageCall.req.Message.GetRequestToLogin().Verifier.Value, savedProtoMessage.GetRequestToLogin().Verifier.Value) - assert.Equal(t, sendMessageCall.req.Message.GetRequestToLogin().Domain.Value, savedProtoMessage.GetRequestToLogin().Domain.Value) - assert.Equal(t, sendMessageCall.req.Message.GetRequestToLogin().Signature.Value, savedProtoMessage.GetRequestToLogin().Signature.Value) - assert.Equal(t, sendMessageCall.req.Message.GetRequestToLogin().RendezvousKey.Value, savedProtoMessage.GetRequestToLogin().RendezvousKey.Value) - assert.Equal(t, sendMessageCall.req.Signature.Value, savedProtoMessage.SendMessageRequestSignature.Value) - - env.server1.assertLoginRequestRecordSaved(t, rendezvousKey, sendMessageCall.req.Message.GetRequestToLogin()) - - env.client1.openMessageStream(t, rendezvousKey, false) - messages := env.client1.receiveMessagesInRealTime(t, rendezvousKey) - env.client1.closeMessageStream(t, rendezvousKey) - require.Len(t, messages, 1) - assert.True(t, proto.Equal(&savedProtoMessage, messages[0])) -} - -func TestSendMessage_RequestToLogin_Validation(t *testing.T) { - env, cleanup := setup(t, false) - defer cleanup() - - rendezvousKey := testutil.NewRandomAccount(t) - - // - // Part 1: Domain validation - - env.client1.resetConf() - env.client1.conf.simulateInvalidDomain = true - sendMessageCall := env.client1.sendRequestToLoginMessage(t, rendezvousKey) - sendMessageCall.assertInvalidMessageError(t, "domain is invalid") - env.server1.assertNoMessages(t, rendezvousKey) - env.server1.assertRequestRecordNotSaved(t, rendezvousKey) - - env.client1.resetConf() - env.client1.conf.simulateDoesntOwnDomain = true - sendMessageCall = env.client1.sendRequestToLoginMessage(t, rendezvousKey) - sendMessageCall.assertPermissionDeniedError(t, "does not own domain getcode.com") - env.server1.assertNoMessages(t, rendezvousKey) - env.server1.assertRequestRecordNotSaved(t, rendezvousKey) - - // - // Part 2: Signature validation - // - - env.client1.resetConf() - env.client1.conf.simulateInvalidMessageSignature = true - sendMessageCall = env.client1.sendRequestToLoginMessage(t, rendezvousKey) - sendMessageCall.assertUnauthenticatedError(t, "") - env.server1.assertNoMessages(t, rendezvousKey) - env.server1.assertRequestRecordNotSaved(t, rendezvousKey) - - // - // Part 3: Rendezvous key validation - // - - env.client1.resetConf() - env.client1.conf.simulateInvalidRendezvousKey = true - sendMessageCall = env.client1.sendRequestToLoginMessage(t, rendezvousKey) - sendMessageCall.assertInvalidMessageError(t, "rendezvous key mismatch") - env.server1.assertNoMessages(t, rendezvousKey) - env.server1.assertRequestRecordNotSaved(t, rendezvousKey) - - // - // Part 4: Downgrading initial payment requirement - // - - env.client1.resetConf() - originalSendMessageRequest := env.client1.sendRequestToReceiveFiatBillMessage(t, rendezvousKey, &testRequestToReceiveBillConf{}) - originalSendMessageRequest.requireSuccess(t) - env.client1.sendRequestToLoginMessage(t, rendezvousKey).assertInvalidMessageError(t, "original request requires payment") - env.server1.assertPaymentRequestRecordSaved(t, rendezvousKey, originalSendMessageRequest.req.Message.GetRequestToReceiveBill()) - messages := env.client1.pollForMessages(t, rendezvousKey) - require.Len(t, messages, 1) - require.NotNil(t, messages[0].GetRequestToReceiveBill()) } +*/ func TestSendMessage_InvalidRendezvousKeySignature(t *testing.T) { env, cleanup := setup(t, false) diff --git a/pkg/code/server/grpc/messaging/stream.go b/pkg/code/server/messaging/stream.go similarity index 100% rename from pkg/code/server/grpc/messaging/stream.go rename to pkg/code/server/messaging/stream.go diff --git a/pkg/code/server/grpc/messaging/testutil.go b/pkg/code/server/messaging/testutil.go similarity index 89% rename from pkg/code/server/grpc/messaging/testutil.go rename to pkg/code/server/messaging/testutil.go index 4968ab2a..a53d7499 100644 --- a/pkg/code/server/grpc/messaging/testutil.go +++ b/pkg/code/server/messaging/testutil.go @@ -30,7 +30,6 @@ import ( "github.com/code-payments/code-server/pkg/code/data/rendezvous" exchange_rate_util "github.com/code-payments/code-server/pkg/code/exchangerate" "github.com/code-payments/code-server/pkg/code/thirdparty" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/testutil" ) @@ -178,26 +177,6 @@ func (s *serverEnv) assertPaymentRequestRecordSaved(t *testing.T, rendezvousKey } } -func (s *serverEnv) assertLoginRequestRecordSaved(t *testing.T, rendezvousKey *common.Account, msg *messagingpb.RequestToLogin) { - asciiBaseDomain, err := thirdparty.GetAsciiBaseDomain(msg.Domain.Value) - require.NoError(t, err) - - requestRecord, err := s.server.data.GetRequest(s.ctx, rendezvousKey.PublicKey().ToBase58()) - require.NoError(t, err) - - assert.Equal(t, requestRecord.Intent, rendezvousKey.PublicKey().ToBase58()) - - require.NotNil(t, requestRecord.Domain) - assert.Equal(t, asciiBaseDomain, *requestRecord.Domain) - assert.True(t, requestRecord.IsVerified) - - assert.Nil(t, requestRecord.DestinationTokenAccount) - assert.Nil(t, requestRecord.ExchangeCurrency) - assert.Nil(t, requestRecord.NativeAmount) - assert.Nil(t, requestRecord.ExchangeRate) - assert.Nil(t, requestRecord.Quantity) -} - func (s *serverEnv) assertRequestRecordNotSaved(t *testing.T, rendezvousKey *common.Account) { _, err := s.server.data.GetRequest(s.ctx, rendezvousKey.PublicKey().ToBase58()) assert.Equal(t, paymentrequest.ErrPaymentRequestNotFound, err) @@ -537,7 +516,7 @@ func (c *clientEnv) sendRequestToGrabBillMessage(t *testing.T, rendezvousKey *co OwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_TEMPORARY_INCOMING, Index: 0, } @@ -553,7 +532,7 @@ func (c *clientEnv) sendRequestToGrabBillMessage(t *testing.T, rendezvousKey *co OwnerAccount: accountInfoRecord.OwnerAccount, AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_TEMPORARY_INCOMING, Index: accountInfoRecord.Index + 1, } @@ -603,7 +582,7 @@ func (c *clientEnv) sendRequestToReceiveKinBillMessage( OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: owner.PublicKey().ToBase58(), TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_PRIMARY, Index: 0, } @@ -613,7 +592,7 @@ func (c *clientEnv) sendRequestToReceiveKinBillMessage( OwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_RELATIONSHIP, Index: 0, RelationshipTo: pointer.String("getcode.com"), @@ -629,7 +608,7 @@ func (c *clientEnv) sendRequestToReceiveKinBillMessage( OwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_TEMPORARY_INCOMING, Index: 0, } @@ -637,10 +616,10 @@ func (c *clientEnv) sendRequestToReceiveKinBillMessage( } exchangeData := &transactionpb.ExchangeData{ - Currency: "kin", + Currency: string(common.CoreMintSymbol), ExchangeRate: 1.0, - NativeAmount: 10_001, - Quarks: kin.ToQuarks(10_001), + NativeAmount: 2, + Quarks: common.ToCoreMintQuarks(2), } if c.conf.simulateInvalidCurrency { @@ -654,18 +633,12 @@ func (c *clientEnv) sendRequestToReceiveKinBillMessage( exchangeData.NativeAmount += 2 } if c.conf.simulateSmallNativeAmount { - exchangeData.NativeAmount = 1 - exchangeData.Quarks = kin.ToQuarks(1) + exchangeData.NativeAmount = 0.01 + exchangeData.Quarks = common.CoreMintQuarksPerUnit / 100 } if c.conf.simulateLargeNativeAmount { - exchangeData.NativeAmount = 250_001 - exchangeData.Quarks = kin.ToQuarks(250_001) - } - if c.conf.simulateFractionalNativeAmount { - exchangeData.NativeAmount += 0.1 - } - if c.conf.simulateFractionalQuarkAmount { - exchangeData.Quarks += kin.QuarksPerKin / 10 + exchangeData.NativeAmount = 10 + exchangeData.Quarks = common.ToCoreMintQuarks(10) } additionalFees := []*transactionpb.AdditionalFeePayment{ @@ -704,7 +677,7 @@ func (c *clientEnv) sendRequestToReceiveKinBillMessage( OwnerAccount: feeCodeAccountOwner.PublicKey().ToBase58(), AuthorityAccount: feeCodeAccountAuthority.PublicKey().ToBase58(), TokenAccount: base58.Encode(additionalFees[0].Destination.Value), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: feeCodeAccountType, Index: 0, })) @@ -717,7 +690,7 @@ func (c *clientEnv) sendRequestToReceiveKinBillMessage( OwnerAccount: feeCodeAccountOwner.PublicKey().ToBase58(), AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: base58.Encode(additionalFees[1].Destination.Value), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_RELATIONSHIP, Index: 0, RelationshipTo: &feeRelationship, @@ -800,7 +773,7 @@ func (c *clientEnv) sendRequestToReceiveFiatBillMessage( OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: owner.PublicKey().ToBase58(), TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_PRIMARY, Index: 0, })) @@ -809,7 +782,7 @@ func (c *clientEnv) sendRequestToReceiveFiatBillMessage( OwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_RELATIONSHIP, Index: 0, RelationshipTo: pointer.String("getcode.com"), @@ -825,7 +798,7 @@ func (c *clientEnv) sendRequestToReceiveFiatBillMessage( OwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_TEMPORARY_INCOMING, Index: 0, } @@ -837,7 +810,7 @@ func (c *clientEnv) sendRequestToReceiveFiatBillMessage( NativeAmount: .50, } if c.conf.simulateInvalidCurrency { - exchangeData.Currency = "kin" + exchangeData.Currency = string(common.CoreMintSymbol) } if c.conf.simulateSmallNativeAmount { exchangeData.NativeAmount = 0.01 @@ -882,7 +855,7 @@ func (c *clientEnv) sendRequestToReceiveFiatBillMessage( OwnerAccount: feeCodeAccountOwner.PublicKey().ToBase58(), AuthorityAccount: feeCodeAccountAuthority.PublicKey().ToBase58(), TokenAccount: base58.Encode(additionalFees[0].Destination.Value), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: feeCodeAccountType, Index: 0, })) @@ -895,7 +868,7 @@ func (c *clientEnv) sendRequestToReceiveFiatBillMessage( OwnerAccount: feeCodeAccountOwner.PublicKey().ToBase58(), AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: base58.Encode(additionalFees[1].Destination.Value), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_RELATIONSHIP, Index: 0, RelationshipTo: &feeRelationship, @@ -957,56 +930,6 @@ func (c *clientEnv) sendRequestToReceiveFiatBillMessage( return c.sendMessage(t, req, rendezvousKey) } -func (c *clientEnv) sendRequestToLoginMessage(t *testing.T, rendezvousKey *common.Account) *sendMessageCallMetadata { - authority, err := common.NewAccountFromPrivateKeyString("dr2MUzL4NCS45qyp16vDXiSdHqqdg2DF79xKaYMB1vzVtDDjPvyQ8xTH4VsTWXSDP3NFzsdCV6gEoChKftzwLno") - require.NoError(t, err) - - if c.conf.simulateDoesntOwnDomain { - authority = testutil.NewRandomAccount(t) - } - - msg := &messagingpb.RequestToLogin{ - Verifier: authority.ToProto(), - Domain: &commonpb.Domain{ - Value: "app.getcode.com", - }, - RendezvousKey: &messagingpb.RendezvousKey{ - Value: rendezvousKey.PublicKey().ToBytes(), - }, - } - - if c.conf.simulateInvalidDomain { - msg.Domain.Value = "localhost" - } - - if c.conf.simulateInvalidRendezvousKey { - msg.RendezvousKey.Value = testutil.NewRandomAccount(t).PublicKey().ToBytes() - } - - messageBytes, err := proto.Marshal(msg) - require.NoError(t, err) - - signer := authority - if c.conf.simulateInvalidMessageSignature { - signer = testutil.NewRandomAccount(t) - } - msg.Signature = &commonpb.Signature{ - Value: ed25519.Sign(signer.PrivateKey().ToBytes(), messageBytes), - } - - req := &messagingpb.SendMessageRequest{ - Message: &messagingpb.Message{ - Kind: &messagingpb.Message_RequestToLogin{ - RequestToLogin: msg, - }, - }, - RendezvousKey: &messagingpb.RendezvousKey{ - Value: rendezvousKey.PublicKey().ToBytes(), - }, - } - return c.sendMessage(t, req, rendezvousKey) -} - func (c *clientEnv) sendMessage(t *testing.T, req *messagingpb.SendMessageRequest, rendezvousKey *common.Account) *sendMessageCallMetadata { messageBytes, err := proto.Marshal(req.Message) require.NoError(t, err) diff --git a/pkg/code/server/micropayment/server.go b/pkg/code/server/micropayment/server.go new file mode 100644 index 00000000..beeeae58 --- /dev/null +++ b/pkg/code/server/micropayment/server.go @@ -0,0 +1,169 @@ +package micropayment + +import ( + "context" + "time" + + "github.com/mr-tron/base58" + "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + + messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" + micropaymentpb "github.com/code-payments/code-protobuf-api/generated/go/micropayment/v1" + + auth_util "github.com/code-payments/code-server/pkg/code/auth" + code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/code/data/paymentrequest" + "github.com/code-payments/code-server/pkg/code/data/webhook" + "github.com/code-payments/code-server/pkg/grpc/client" + "github.com/code-payments/code-server/pkg/netutil" +) + +const ( + codifiedContentUrlBase = "getcode.com/m/" +) + +type microPaymentServer struct { + log *logrus.Entry + + data code_data.Provider + + auth *auth_util.RPCSignatureVerifier + + micropaymentpb.UnimplementedMicroPaymentServer +} + +func NewMicroPaymentServer( + data code_data.Provider, + auth *auth_util.RPCSignatureVerifier, +) micropaymentpb.MicroPaymentServer { + return µPaymentServer{ + log: logrus.StandardLogger().WithField("type", "micropayment/v1/server"), + data: data, + auth: auth, + } +} + +func (s *microPaymentServer) GetStatus(ctx context.Context, req *micropaymentpb.GetStatusRequest) (*micropaymentpb.GetStatusResponse, error) { + log := s.log.WithField("method", "GetStatus") + log = client.InjectLoggingMetadata(ctx, log) + + intentId := base58.Encode(req.IntentId.Value) + log = log.WithField("intent", intentId) + + resp := µpaymentpb.GetStatusResponse{ + Exists: false, + CodeScanned: false, + IntentSubmitted: false, + } + + _, err := s.data.GetRequest(ctx, intentId) + if err == paymentrequest.ErrPaymentRequestNotFound { + return resp, nil + } else if err != nil { + log.WithError(err).Warn("failure getting request record") + return nil, status.Error(codes.Internal, "") + } + resp.Exists = true + + messageRecords, err := s.data.GetMessages(ctx, intentId) + if err != nil { + log.WithError(err).Warn("failure getting message records") + return nil, status.Error(codes.Internal, "") + } + + for _, messageRecord := range messageRecords { + var message messagingpb.Message + if err := proto.Unmarshal(messageRecord.Message, &message); err != nil { + log.WithError(err).Warn("failure unmarshalling message bytes") + continue + } + + switch message.Kind.(type) { + case *messagingpb.Message_CodeScanned: + resp.CodeScanned = true + } + + if resp.CodeScanned { + break + } + } + + intentRecord, err := s.data.GetIntent(ctx, intentId) + switch err { + case nil: + resp.IntentSubmitted = intentRecord.State != intent.StateRevoked + case intent.ErrIntentNotFound: + default: + log.WithError(err).Warn("failure getting intent record") + return nil, status.Error(codes.Internal, "") + } + + return resp, nil +} + +func (s *microPaymentServer) RegisterWebhook(ctx context.Context, req *micropaymentpb.RegisterWebhookRequest) (*micropaymentpb.RegisterWebhookResponse, error) { + log := s.log.WithField("method", "RegisterWebhook") + log = client.InjectLoggingMetadata(ctx, log) + + intentId := base58.Encode(req.IntentId.Value) + log = log.WithField("intent", intentId) + + err := netutil.ValidateHttpUrl(req.Url, false, false) + if err != nil { + log.WithField("url", req.Url).WithError(err).Info("url failed validation") + return µpaymentpb.RegisterWebhookResponse{ + Result: micropaymentpb.RegisterWebhookResponse_INVALID_URL, + }, nil + } + + // todo: distributed lock on intent id + + _, err = s.data.GetIntent(ctx, intentId) + if err == nil { + return µpaymentpb.RegisterWebhookResponse{ + Result: micropaymentpb.RegisterWebhookResponse_INTENT_EXISTS, + }, nil + } else if err != intent.ErrIntentNotFound { + log.WithError(err).Warn("failure checking intent status") + return nil, status.Error(codes.Internal, "") + } + + _, err = s.data.GetRequest(ctx, intentId) + if err == paymentrequest.ErrPaymentRequestNotFound { + return µpaymentpb.RegisterWebhookResponse{ + Result: micropaymentpb.RegisterWebhookResponse_REQUEST_NOT_FOUND, + }, nil + } else if err != nil { + log.WithError(err).Warn("failure checking request status") + return nil, status.Error(codes.Internal, "") + } + + record := &webhook.Record{ + WebhookId: intentId, + Url: req.Url, + Type: webhook.TypeIntentSubmitted, + + Attempts: 0, + State: webhook.StateUnknown, + + CreatedAt: time.Now(), + NextAttemptAt: nil, + } + err = s.data.CreateWebhook(ctx, record) + if err == webhook.ErrAlreadyExists { + return µpaymentpb.RegisterWebhookResponse{ + Result: micropaymentpb.RegisterWebhookResponse_ALREADY_REGISTERED, + }, nil + } else if err != nil { + log.WithError(err).Warn("failure creating webhook record") + return nil, status.Error(codes.Internal, "") + } + + return µpaymentpb.RegisterWebhookResponse{ + Result: micropaymentpb.RegisterWebhookResponse_OK, + }, nil +} diff --git a/pkg/code/server/grpc/micropayment/server_test.go b/pkg/code/server/micropayment/server_test.go similarity index 56% rename from pkg/code/server/grpc/micropayment/server_test.go rename to pkg/code/server/micropayment/server_test.go index 39f4487f..e9430321 100644 --- a/pkg/code/server/grpc/micropayment/server_test.go +++ b/pkg/code/server/micropayment/server_test.go @@ -2,14 +2,11 @@ package micropayment import ( "context" - "crypto/ed25519" "fmt" - "strings" "testing" "github.com/golang/protobuf/proto" "github.com/google/uuid" - "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -23,13 +20,11 @@ import ( auth_util "github.com/code-payments/code-server/pkg/code/auth" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/messaging" "github.com/code-payments/code-server/pkg/code/data/paymentrequest" "github.com/code-payments/code-server/pkg/code/data/webhook" currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/testutil" ) @@ -48,19 +43,15 @@ func TestGetStatus_Flags_HappyPath(t *testing.T) { ExchangeCurrency: pointer.String(string(currency_lib.USD)), NativeAmount: pointer.Float64(1.0), } - loginRequestRecord := &paymentrequest.Record{ - Domain: pointer.String("example.com"), - IsVerified: true, - } paymentIntentRecord := &intent.Record{ IntentType: intent.SendPrivatePayment, - SendPrivatePaymentMetadata: &intent.SendPrivatePaymentMetadata{ + SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ ExchangeCurrency: currency_lib.Code(*paymentRequestRecord.ExchangeCurrency), NativeAmount: *paymentRequestRecord.NativeAmount, ExchangeRate: 0.1, - Quantity: kin.ToQuarks(10), + Quantity: common.ToCoreMintQuarks(10), UsdMarketValue: *paymentRequestRecord.NativeAmount, DestinationTokenAccount: *paymentRequestRecord.DestinationTokenAccount, @@ -73,25 +64,11 @@ func TestGetStatus_Flags_HappyPath(t *testing.T) { State: intent.StatePending, } - loginIntentRecord := &intent.Record{ - IntentType: intent.Login, - - LoginMetadata: &intent.LoginMetadata{ - App: "example.com", - UserId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - }, - - InitiatorOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - - State: intent.StateConfirmed, - } - for _, tc := range []struct { requestRecord *paymentrequest.Record intentRecord *intent.Record }{ {paymentRequestRecord, paymentIntentRecord}, - {loginRequestRecord, loginIntentRecord}, } { env, cleanup := setup(t) defer cleanup() @@ -331,232 +308,6 @@ func TestRegisterWebhook_UrlValidation(t *testing.T) { assert.Equal(t, webhook.ErrNotFound, err) } -func TestCodify_HappyPath(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - destination := testutil.NewRandomAccount(t) - - accountInfoRecord := &account.Record{ - OwnerAccount: owner.PublicKey().ToBase58(), - AuthorityAccount: owner.PublicKey().ToBase58(), - TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_PRIMARY, - } - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) - - codifyReq := µpaymentpb.CodifyRequest{ - OwnerAccount: owner.ToProto(), - PrimaryAccount: destination.ToProto(), - Currency: "usd", - NativeAmount: 0.25, - Url: "http://getcode.com", - } - - reqBytes, err := proto.Marshal(codifyReq) - require.NoError(t, err) - - codifyReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(owner.PrivateKey().ToBytes(), reqBytes), - } - - codifyResp, err := env.client.Codify(env.ctx, codifyReq) - require.NoError(t, err) - assert.Equal(t, micropaymentpb.CodifyResponse_OK, codifyResp.Result) - assert.True(t, strings.HasPrefix(codifyResp.CodifiedUrl, codifiedContentUrlBase)) - assert.True(t, len(codifyResp.CodifiedUrl) > len(codifiedContentUrlBase)+4) - - shortPath := strings.Replace(codifyResp.CodifiedUrl, codifiedContentUrlBase, "", 1) - - paywallRecord, err := env.data.GetPaywallByShortPath(env.ctx, shortPath) - require.NoError(t, err) - assert.Equal(t, owner.PublicKey().ToBase58(), paywallRecord.OwnerAccount) - assert.Equal(t, destination.PublicKey().ToBase58(), paywallRecord.DestinationTokenAccount) - assert.EqualValues(t, codifyReq.Currency, paywallRecord.ExchangeCurrency) - assert.Equal(t, codifyReq.NativeAmount, paywallRecord.NativeAmount) - assert.Equal(t, codifyReq.Url, paywallRecord.RedirectUrl) - assert.Equal(t, base58.Encode(codifyReq.Signature.Value), paywallRecord.Signature) - - getPathMetadataResp, err := env.client.GetPathMetadata(env.ctx, µpaymentpb.GetPathMetadataRequest{ - Path: getRandomShortPath(), - }) - require.NoError(t, err) - assert.Equal(t, micropaymentpb.GetPathMetadataResponse_NOT_FOUND, getPathMetadataResp.Result) - - getPathMetadataResp, err = env.client.GetPathMetadata(env.ctx, µpaymentpb.GetPathMetadataRequest{ - Path: shortPath, - }) - require.NoError(t, err) - assert.Equal(t, micropaymentpb.GetPathMetadataResponse_OK, getPathMetadataResp.Result) - assert.Equal(t, destination.PublicKey().ToBytes(), getPathMetadataResp.Destination.Value) - assert.Equal(t, codifyReq.Currency, getPathMetadataResp.Currency) - assert.Equal(t, codifyReq.NativeAmount, getPathMetadataResp.NativeAmount) - assert.Equal(t, codifyReq.Url, getPathMetadataResp.RedirctUrl) -} - -func TestCodify_AccountValidation(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - destination := testutil.NewRandomAccount(t) - - codifyReq := µpaymentpb.CodifyRequest{ - OwnerAccount: owner.ToProto(), - PrimaryAccount: destination.ToProto(), - Currency: "usd", - NativeAmount: 0.25, - Url: "http://getcode.com", - } - - reqBytes, err := proto.Marshal(codifyReq) - require.NoError(t, err) - - codifyReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(owner.PrivateKey().ToBytes(), reqBytes), - } - - // - // External accounts not allowed - // - - codifyResp, err := env.client.Codify(env.ctx, codifyReq) - require.NoError(t, err) - assert.Equal(t, micropaymentpb.CodifyResponse_INVALID_ACCOUNT, codifyResp.Result) - assert.Empty(t, codifyResp.CodifiedUrl) - - // - // Non-primary accounts not allowed - // - - accountInfoRecord := &account.Record{ - OwnerAccount: owner.PublicKey().ToBase58(), - AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_TEMPORARY_INCOMING, - } - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) - - codifyResp, err = env.client.Codify(env.ctx, codifyReq) - require.NoError(t, err) - assert.Equal(t, micropaymentpb.CodifyResponse_INVALID_ACCOUNT, codifyResp.Result) - assert.Empty(t, codifyResp.CodifiedUrl) -} - -func TestCodify_UrlValidation(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - for _, invalidUrl := range append( - baseInvalidUrlsToTest, - "http://getcode.com/not-found", - ) { - owner := testutil.NewRandomAccount(t) - codifyReq := µpaymentpb.CodifyRequest{ - OwnerAccount: owner.ToProto(), - PrimaryAccount: testutil.NewRandomAccount(t).ToProto(), - Currency: "usd", - NativeAmount: 0.25, - Url: invalidUrl, - } - - reqBytes, err := proto.Marshal(codifyReq) - require.NoError(t, err) - - codifyReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(owner.PrivateKey().ToBytes(), reqBytes), - } - - codifyResp, err := env.client.Codify(env.ctx, codifyReq) - if err != nil { - testutil.AssertStatusErrorWithCode(t, err, codes.InvalidArgument) - } else { - require.NoError(t, err) - assert.Equal(t, micropaymentpb.CodifyResponse_INVALID_URL, codifyResp.Result) - assert.Empty(t, codifyResp.CodifiedUrl) - } - } -} - -func TestCodify_AmountValidation(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - destination := testutil.NewRandomAccount(t) - - accountInfoRecord := &account.Record{ - OwnerAccount: owner.PublicKey().ToBase58(), - AuthorityAccount: owner.PublicKey().ToBase58(), - TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_PRIMARY, - } - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) - - for _, amount := range []float64{0.01, 5.01} { - codifyReq := µpaymentpb.CodifyRequest{ - OwnerAccount: owner.ToProto(), - PrimaryAccount: destination.ToProto(), - Currency: "usd", - NativeAmount: amount, - Url: "http://getcode.com", - } - - reqBytes, err := proto.Marshal(codifyReq) - require.NoError(t, err) - - codifyReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(owner.PrivateKey().ToBytes(), reqBytes), - } - - codifyResp, err := env.client.Codify(env.ctx, codifyReq) - require.NoError(t, err) - assert.Equal(t, micropaymentpb.CodifyResponse_NATIVE_AMOUNT_EXCEEDS_LIMIT, codifyResp.Result) - assert.Empty(t, codifyResp.CodifiedUrl) - } -} - -func TestCodify_CurrencyValidation(t *testing.T) { - env, cleanup := setup(t) - defer cleanup() - - owner := testutil.NewRandomAccount(t) - destination := testutil.NewRandomAccount(t) - - accountInfoRecord := &account.Record{ - OwnerAccount: owner.PublicKey().ToBase58(), - AuthorityAccount: owner.PublicKey().ToBase58(), - TokenAccount: destination.PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), - AccountType: commonpb.AccountType_PRIMARY, - } - require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) - - codifyReq := µpaymentpb.CodifyRequest{ - OwnerAccount: owner.ToProto(), - PrimaryAccount: destination.ToProto(), - Currency: "btc", - NativeAmount: 1, - Url: "http://getcode.com", - } - - reqBytes, err := proto.Marshal(codifyReq) - require.NoError(t, err) - - codifyReq.Signature = &commonpb.Signature{ - Value: ed25519.Sign(owner.PrivateKey().ToBytes(), reqBytes), - } - - codifyResp, err := env.client.Codify(env.ctx, codifyReq) - require.NoError(t, err) - assert.Equal(t, micropaymentpb.CodifyResponse_UNSUPPORTED_CURRENCY, codifyResp.Result) - assert.Empty(t, codifyResp.CodifiedUrl) -} - type testEnv struct { ctx context.Context client micropaymentpb.MicroPaymentClient diff --git a/pkg/code/server/transaction/action_handler.go b/pkg/code/server/transaction/action_handler.go new file mode 100644 index 00000000..ad88a162 --- /dev/null +++ b/pkg/code/server/transaction/action_handler.go @@ -0,0 +1,448 @@ +package transaction_v2 + +import ( + "context" + "errors" + "time" + + commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" + transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" + + "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/code/data/account" + "github.com/code-payments/code-server/pkg/code/data/action" + "github.com/code-payments/code-server/pkg/code/data/fulfillment" + "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/code/data/timelock" + "github.com/code-payments/code-server/pkg/solana" + "github.com/code-payments/code-server/pkg/solana/cvm" +) + +type newFulfillmentMetadata struct { + // Signature metadata + + requiresClientSignature bool + expectedSigner *common.Account // Must be null if the requiresClientSignature is false + virtualIxnHash *cvm.CompactMessage // Must be null if the requiresClientSignature is false + + // Additional metadata to add to the action and fulfillment record, which relates + // specifically to the transaction or virtual instruction within the context of + // the action. + + fulfillmentType fulfillment.Type + + source *common.Account + destination *common.Account + + fulfillmentOrderingIndex uint32 + disableActiveScheduling bool +} + +// BaseActionHandler is a base interface for operation-specific action handlers +// +// Note: Action handlers should load all required state on initialization to +// avoid duplicated work across interface method calls. +type BaseActionHandler interface { + // GetServerParameter gets the server parameter for the action within the context + // of the intent. + GetServerParameter() *transactionpb.ServerParameter + + // OnSaveToDB is a callback when the action is being saved to the DB + // within the scope of a DB transaction. Additional supporting DB records + // (ie. not the action or fulfillment records) relevant to the action should + // be saved here. + OnSaveToDB(ctx context.Context) error +} + +// CreateActionHandler is an interface for creating new actions +type CreateActionHandler interface { + BaseActionHandler + + // FulfillmentCount returns the total number of fulfillments that + // will be created for the action. + FulfillmentCount() int + + // PopulateMetadata populates action metadata into the provided record + PopulateMetadata(actionRecord *action.Record) error + + // RequiresNonce determines whether a nonce should be acquired for the + // fulfillment being created. This should be true whenever a virtual + // instruction needs to be signed by the client. + RequiresNonce(fulfillmentIndex int) bool + + // GetFulfillmentMetadata gets metadata for the fulfillment being created + GetFulfillmentMetadata( + index int, + nonce *common.Account, + bh solana.Blockhash, + ) (*newFulfillmentMetadata, error) +} + +// UpgradeActionHandler is an interface for upgrading existing actions. It's +// assumed we'll only be upgrading a single fulfillment. +type UpgradeActionHandler interface { + BaseActionHandler + + // GetFulfillmentBeingUpgraded gets the original fulfillment that's being + // upgraded. + GetFulfillmentBeingUpgraded() *fulfillment.Record + + // GetFulfillmentMetadata gets upgraded fulfillment metadata + GetFulfillmentMetadata( + nonce *common.Account, + bh solana.Blockhash, + ) (*newFulfillmentMetadata, error) +} + +type OpenAccountActionHandler struct { + data code_data.Provider + + accountType commonpb.AccountType + timelockAccounts *common.TimelockAccounts + + unsavedAccountInfoRecord *account.Record + unsavedTimelockRecord *timelock.Record +} + +func NewOpenAccountActionHandler(data code_data.Provider, protoAction *transactionpb.OpenAccountAction, protoMetadata *transactionpb.Metadata) (CreateActionHandler, error) { + owner, err := common.NewAccountFromProto(protoAction.Owner) + if err != nil { + return nil, err + } + + authority, err := common.NewAccountFromProto(protoAction.Authority) + if err != nil { + return nil, err + } + + timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + if err != nil { + return nil, err + } + + unsavedAccountInfoRecord := &account.Record{ + OwnerAccount: owner.PublicKey().ToBase58(), + AuthorityAccount: authority.PublicKey().ToBase58(), + TokenAccount: timelockAccounts.Vault.PublicKey().ToBase58(), + MintAccount: timelockAccounts.Mint.PublicKey().ToBase58(), + AccountType: protoAction.AccountType, + Index: protoAction.Index, + DepositsLastSyncedAt: time.Now(), + RequiresDepositSync: false, + RequiresAutoReturnCheck: protoAction.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD, + } + + unsavedTimelockRecord := timelockAccounts.ToDBRecord() + + return &OpenAccountActionHandler{ + data: data, + + accountType: protoAction.AccountType, + timelockAccounts: timelockAccounts, + + unsavedAccountInfoRecord: unsavedAccountInfoRecord, + unsavedTimelockRecord: unsavedTimelockRecord, + }, nil +} + +func (h *OpenAccountActionHandler) FulfillmentCount() int { + return 1 +} + +func (h *OpenAccountActionHandler) PopulateMetadata(actionRecord *action.Record) error { + actionRecord.Source = h.timelockAccounts.Vault.PublicKey().ToBase58() + + actionRecord.State = action.StatePending + + return nil +} + +func (h *OpenAccountActionHandler) GetServerParameter() *transactionpb.ServerParameter { + return &transactionpb.ServerParameter{ + Type: &transactionpb.ServerParameter_OpenAccount{ + OpenAccount: &transactionpb.OpenAccountServerParameter{}, + }, + } +} + +func (h *OpenAccountActionHandler) RequiresNonce(index int) bool { + return false +} + +func (h *OpenAccountActionHandler) GetFulfillmentMetadata( + index int, + nonce *common.Account, + bh solana.Blockhash, +) (*newFulfillmentMetadata, error) { + switch index { + case 0: + return &newFulfillmentMetadata{ + requiresClientSignature: false, + expectedSigner: nil, + virtualIxnHash: nil, + + fulfillmentType: fulfillment.InitializeLockedTimelockAccount, + source: h.timelockAccounts.Vault, + destination: nil, + fulfillmentOrderingIndex: 0, + disableActiveScheduling: h.accountType != commonpb.AccountType_PRIMARY, // Non-primary accounts are created on demand after first usage + }, nil + default: + return nil, errors.New("invalid virtual ixn index") + } +} + +func (h *OpenAccountActionHandler) OnSaveToDB(ctx context.Context) error { + err := h.data.SaveTimelock(ctx, h.unsavedTimelockRecord) + if err != nil { + return err + } + + return h.data.CreateAccountInfo(ctx, h.unsavedAccountInfoRecord) +} + +type NoPrivacyTransferActionHandler struct { + source *common.TimelockAccounts + destination *common.Account + amount uint64 + isFeePayment bool // Internally, the mechanics of a fee payment are exactly the same + isCodeFeePayment bool +} + +func NewNoPrivacyTransferActionHandler(protoAction *transactionpb.NoPrivacyTransferAction) (CreateActionHandler, error) { + sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) + if err != nil { + return nil, err + } + + source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + if err != nil { + return nil, err + } + + destination, err := common.NewAccountFromProto(protoAction.Destination) + if err != nil { + return nil, err + } + + return &NoPrivacyTransferActionHandler{ + source: source, + destination: destination, + amount: protoAction.Amount, + isFeePayment: false, + }, nil +} + +func NewFeePaymentActionHandler(protoAction *transactionpb.FeePaymentAction, feeCollector *common.Account) (CreateActionHandler, error) { + sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) + if err != nil { + return nil, err + } + + source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + if err != nil { + return nil, err + } + + var destination *common.Account + var isCodeFeePayment bool + if protoAction.Type == transactionpb.FeePaymentAction_CODE { + destination = feeCollector + isCodeFeePayment = true + } else { + destination, err = common.NewAccountFromProto(protoAction.Destination) + if err != nil { + return nil, err + } + } + + return &NoPrivacyTransferActionHandler{ + source: source, + destination: destination, + amount: protoAction.Amount, + isFeePayment: true, + isCodeFeePayment: isCodeFeePayment, + }, nil +} + +func (h *NoPrivacyTransferActionHandler) FulfillmentCount() int { + return 1 +} + +func (h *NoPrivacyTransferActionHandler) PopulateMetadata(actionRecord *action.Record) error { + actionRecord.Source = h.source.Vault.PublicKey().ToBase58() + + destination := h.destination.PublicKey().ToBase58() + actionRecord.Destination = &destination + + actionRecord.Quantity = &h.amount + + actionRecord.State = action.StatePending + + return nil +} +func (h *NoPrivacyTransferActionHandler) GetServerParameter() *transactionpb.ServerParameter { + if h.isFeePayment { + var codeDestination *commonpb.SolanaAccountId + if h.isCodeFeePayment { + codeDestination = h.destination.ToProto() + } + + return &transactionpb.ServerParameter{ + Type: &transactionpb.ServerParameter_FeePayment{ + FeePayment: &transactionpb.FeePaymentServerParameter{ + CodeDestination: codeDestination, + }, + }, + } + } + + return &transactionpb.ServerParameter{ + Type: &transactionpb.ServerParameter_NoPrivacyTransfer{ + NoPrivacyTransfer: &transactionpb.NoPrivacyTransferServerParameter{}, + }, + } +} + +func (h *NoPrivacyTransferActionHandler) RequiresNonce(index int) bool { + return true +} + +func (h *NoPrivacyTransferActionHandler) GetFulfillmentMetadata( + index int, + nonce *common.Account, + bh solana.Blockhash, +) (*newFulfillmentMetadata, error) { + switch index { + case 0: + virtualIxnHash := cvm.GetCompactTransferMessage(&cvm.GetCompactTransferMessageArgs{ + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.destination.PublicKey().ToBytes(), + Amount: h.amount, + NonceAddress: nonce.PublicKey().ToBytes(), + NonceValue: cvm.Hash(bh), + }) + + return &newFulfillmentMetadata{ + requiresClientSignature: true, + expectedSigner: h.source.VaultOwner, + virtualIxnHash: &virtualIxnHash, + + fulfillmentType: fulfillment.NoPrivacyTransferWithAuthority, + source: h.source.Vault, + destination: h.destination, + fulfillmentOrderingIndex: 0, + disableActiveScheduling: h.isFeePayment, + }, nil + default: + return nil, errors.New("invalid transaction index") + } +} + +func (h *NoPrivacyTransferActionHandler) OnSaveToDB(ctx context.Context) error { + return nil +} + +type NoPrivacyWithdrawActionHandler struct { + source *common.TimelockAccounts + destination *common.Account + amount uint64 + disableActiveScheduling bool +} + +func NewNoPrivacyWithdrawActionHandler(intentRecord *intent.Record, protoAction *transactionpb.NoPrivacyWithdrawAction) (CreateActionHandler, error) { + var disableActiveScheduling bool + + switch intentRecord.IntentType { + case intent.SendPrivatePayment: + // Technically we should do this for public receives too, but we don't + // yet have a great way of doing cross intent fulfillment polling hints. + disableActiveScheduling = true + } + + sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) + if err != nil { + return nil, err + } + + source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + if err != nil { + return nil, err + } + + destination, err := common.NewAccountFromProto(protoAction.Destination) + if err != nil { + return nil, err + } + + return &NoPrivacyWithdrawActionHandler{ + source: source, + destination: destination, + amount: protoAction.Amount, + disableActiveScheduling: disableActiveScheduling, + }, nil +} + +func (h *NoPrivacyWithdrawActionHandler) FulfillmentCount() int { + return 1 +} + +func (h *NoPrivacyWithdrawActionHandler) PopulateMetadata(actionRecord *action.Record) error { + actionRecord.Source = h.source.Vault.PublicKey().ToBase58() + + destination := h.destination.PublicKey().ToBase58() + actionRecord.Destination = &destination + + actionRecord.Quantity = &h.amount + + actionRecord.State = action.StatePending + + return nil +} +func (h *NoPrivacyWithdrawActionHandler) GetServerParameter() *transactionpb.ServerParameter { + return &transactionpb.ServerParameter{ + Type: &transactionpb.ServerParameter_NoPrivacyWithdraw{ + NoPrivacyWithdraw: &transactionpb.NoPrivacyWithdrawServerParameter{}, + }, + } +} + +func (h *NoPrivacyWithdrawActionHandler) RequiresNonce(index int) bool { + return true +} + +func (h *NoPrivacyWithdrawActionHandler) GetFulfillmentMetadata( + index int, + nonce *common.Account, + bh solana.Blockhash, +) (*newFulfillmentMetadata, error) { + switch index { + case 0: + virtualIxnHash := cvm.GetCompactWithdrawMessage(&cvm.GetCompactWithdrawMessageArgs{ + Source: h.source.Vault.PublicKey().ToBytes(), + Destination: h.destination.PublicKey().ToBytes(), + NonceAddress: nonce.PublicKey().ToBytes(), + NonceValue: cvm.Hash(bh), + }) + + return &newFulfillmentMetadata{ + requiresClientSignature: true, + expectedSigner: h.source.VaultOwner, + virtualIxnHash: &virtualIxnHash, + + fulfillmentType: fulfillment.NoPrivacyWithdraw, + source: h.source.Vault, + destination: h.destination, + fulfillmentOrderingIndex: 0, + + disableActiveScheduling: h.disableActiveScheduling, + }, nil + default: + return nil, errors.New("invalid transaction index") + } +} + +func (h *NoPrivacyWithdrawActionHandler) OnSaveToDB(ctx context.Context) error { + return nil +} diff --git a/pkg/code/server/grpc/transaction/v2/airdrop.go b/pkg/code/server/transaction/airdrop.go similarity index 85% rename from pkg/code/server/grpc/transaction/v2/airdrop.go rename to pkg/code/server/transaction/airdrop.go index befbfdfc..c3dcf290 100644 --- a/pkg/code/server/grpc/transaction/v2/airdrop.go +++ b/pkg/code/server/transaction/airdrop.go @@ -22,16 +22,13 @@ import ( "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/event" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/nonce" - event_util "github.com/code-payments/code-server/pkg/code/event" exchange_rate_util "github.com/code-payments/code-server/pkg/code/exchangerate" "github.com/code-payments/code-server/pkg/code/transaction" currency_lib "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/solana/cvm" ) @@ -42,15 +39,13 @@ import ( // use an actual client that hits SubmitIntent directly, or update this code to for an // intenral server process (eg. by using the correct nonce pool to avoid race conditions // without distributed locks). -// -// Important Note: We generally assumes 1 account per phone number for simplicity type AirdropType uint8 const ( AirdropTypeUnknown AirdropType = iota - AirdropTypeGiveFirstKin - AirdropTypeGetFirstKin + AirdropTypeGiveFirstCrypto + AirdropTypeGetFirstCrypto ) var ( @@ -91,23 +86,12 @@ func (s *transactionServer) Airdrop(ctx context.Context, req *transactionpb.Aird }, nil } - isEligible, err := s.data.IsEligibleForAirdrop(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting airdrop eligibility for owner account") - return nil, status.Error(codes.Internal, "") - } - if !isEligible { - return &transactionpb.AirdropResponse{ - Result: transactionpb.AirdropResponse_UNAVAILABLE, - }, nil - } - ownerLock := s.ownerLocks.Get(owner.PublicKey().ToBytes()) ownerLock.Lock() defer ownerLock.Unlock() - newIntentId := GetNewAirdropIntentId(AirdropTypeGetFirstKin, owner.PublicKey().ToBase58()) - oldIntentId := GetOldAirdropIntentId(AirdropTypeGetFirstKin, owner.PublicKey().ToBase58()) + newIntentId := GetNewAirdropIntentId(AirdropTypeGetFirstCrypto, owner.PublicKey().ToBase58()) + oldIntentId := GetOldAirdropIntentId(AirdropTypeGetFirstCrypto, owner.PublicKey().ToBase58()) for _, intentId := range []string{ newIntentId, oldIntentId, @@ -130,7 +114,7 @@ func (s *transactionServer) Airdrop(ctx context.Context, req *transactionpb.Aird }, nil } - if req.AirdropType != transactionpb.AirdropType_GET_FIRST_KIN { + if req.AirdropType != transactionpb.AirdropType_GET_FIRST_CRYPTO { return &transactionpb.AirdropResponse{ Result: transactionpb.AirdropResponse_UNAVAILABLE, }, nil @@ -148,7 +132,7 @@ func (s *transactionServer) Airdrop(ctx context.Context, req *transactionpb.Aird } } - intentRecord, err := s.airdrop(ctx, newIntentId, owner, AirdropTypeGetFirstKin) + intentRecord, err := s.airdrop(ctx, newIntentId, owner, AirdropTypeGetFirstCrypto) switch err { case nil: case ErrInsufficientAirdropperBalance, ErrInvalidAirdropTarget, ErrIneligibleForAirdrop: @@ -186,21 +170,14 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner "airdrop_type": airdropType.String(), }) - // Check whether the phone number allowed to receive an airdrop broadly - verificationRecord, err := s.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting phone verification record") - return nil, err - } - var quarkAmount uint64 switch airdropType { - case AirdropTypeGetFirstKin: - quarkAmount = kin.ToQuarks(1) + case AirdropTypeGetFirstCrypto: + quarkAmount = common.ToCoreMintQuarks(1) // todo: configurable default: return nil, errors.New("unhandled airdrop type") } - kinAmount := float64(kin.FromQuarks(quarkAmount)) + coreMintAmount := float64(common.FromCoreMintQuarks(quarkAmount)) // todo: doesn't handle fractional amount // Find the destination account, which will be the user's primary account primaryAccountInfoRecord, err := s.data.GetLatestAccountInfoByOwnerAddressAndType(ctx, owner.PublicKey().ToBase58(), commonpb.AccountType_PRIMARY) @@ -290,10 +267,10 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner DestinationTokenAccount: destination.PublicKey().ToBase58(), Quantity: quarkAmount, - ExchangeCurrency: currency_lib.KIN, + ExchangeCurrency: common.CoreMintSymbol, ExchangeRate: 1.0, - NativeAmount: kinAmount, - UsdMarketValue: usdRateRecord.Rate * kinAmount, + NativeAmount: coreMintAmount, + UsdMarketValue: usdRateRecord.Rate * coreMintAmount, IsWithdrawal: true, }, @@ -349,27 +326,6 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner CreatedAt: time.Now(), } - var eventType event.EventType - switch airdropType { - case AirdropTypeGetFirstKin: - eventType = event.WelcomeBonusClaimed - default: - return nil, errors.Errorf("no event type defined for %s airdrop", airdropType.String()) - } - - eventRecord := &event.Record{ - EventId: intentRecord.IntentId, - EventType: eventType, - - SourceCodeAccount: owner.PublicKey().ToBase58(), - SourceIdentity: verificationRecord.PhoneNumber, - - SpamConfidence: 0, - - CreatedAt: time.Now(), - } - event_util.InjectClientDetails(ctx, s.maxmind, eventRecord, true) - err = s.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { err := s.data.SaveIntent(ctx, intentRecord) if err != nil { @@ -392,11 +348,6 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner return err } - err = s.data.SaveEvent(ctx, eventRecord) - if err != nil { - return err - } - // Intent is pending only after everything's been saved. intentRecord.State = intent.StatePending return s.data.SaveIntent(ctx, intentRecord) @@ -432,7 +383,7 @@ func (s *transactionServer) mustLoadAirdropper(ctx context.Context) { return err } - timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) if err != nil { return err } @@ -464,10 +415,10 @@ func (t AirdropType) String() string { switch t { case AirdropTypeUnknown: return "unknown" - case AirdropTypeGiveFirstKin: - return "give_first_kin" - case AirdropTypeGetFirstKin: - return "get_first_kin" + case AirdropTypeGiveFirstCrypto: + return "give_first_crypto" + case AirdropTypeGetFirstCrypto: + return "get_first_crypto" } return "unknown" } diff --git a/pkg/code/server/grpc/transaction/v2/airdrop_test.go b/pkg/code/server/transaction/airdrop_test.go similarity index 100% rename from pkg/code/server/grpc/transaction/v2/airdrop_test.go rename to pkg/code/server/transaction/airdrop_test.go diff --git a/pkg/code/server/transaction/config.go b/pkg/code/server/transaction/config.go new file mode 100644 index 00000000..b1df526f --- /dev/null +++ b/pkg/code/server/transaction/config.go @@ -0,0 +1,116 @@ +package transaction_v2 + +import ( + "time" + + "github.com/code-payments/code-server/pkg/config" + "github.com/code-payments/code-server/pkg/config/env" + "github.com/code-payments/code-server/pkg/config/memory" + "github.com/code-payments/code-server/pkg/config/wrapper" +) + +const ( + envConfigPrefix = "TRANSACTION_V2_SERVICE_" + + DisableSubmitIntentConfigEnvName = envConfigPrefix + "DISABLE_SUBMIT_INTENT" + defaultDisableSubmitIntent = false + + DisableBlockchainChecksConfigEnvName = envConfigPrefix + "DISABLE_BLOCKCHAIN_CHECKS" + defaultDisableBlockchainChecks = false + + SubmitIntentTimeoutConfigEnvName = envConfigPrefix + "SUBMIT_INTENT_TIMEOUT" + defaultSubmitIntentTimeout = 5 * time.Second + + SwapTimeoutConfigEnvName = envConfigPrefix + "SWAP_TIMEOUT" + defaultSwapTimeout = 60 * time.Second + + SwapPriorityFeeMultiple = envConfigPrefix + "SWAP_PRIORITY_FEE_MULTIPLE" + defaultSwapPriorityFeeMultiple = 1.0 + + ClientReceiveTimeoutConfigEnvName = envConfigPrefix + "CLIENT_RECEIVE_TIMEOUT" + defaultClientReceiveTimeout = time.Second + + FeeCollectorTokenPublicKeyConfigEnvName = envConfigPrefix + "FEE_COLLECTOR_TOKEN_PUBLIC_KEY" + defaultFeeCollectorPublicKey = "invalid" // Ensure something valid is set + + EnableAirdropsConfigEnvName = envConfigPrefix + "ENABLE_AIRDROPS" + defaultEnableAirdrops = false + + AirdropperOwnerPublicKeyEnvName = envConfigPrefix + "AIRDROPPER_OWNER_PUBLIC_KEY" + defaultAirdropperOwnerPublicKey = "invalid" // Ensure something valid is set + + SwapSubsidizerOwnerPublicKeyEnvName = envConfigPrefix + "SWAP_SUBSIDIZER_OWNER_PUBLIC_KEY" + defaultSwapSubsidizerOwnerPublicKey = "invalid" // Ensure something valid is set +) + +type conf struct { + disableSubmitIntent config.Bool + disableAntispamChecks config.Bool // To avoid limits during testing + disableAmlChecks config.Bool // To avoid limits during testing + disableBlockchainChecks config.Bool + submitIntentTimeout config.Duration + swapTimeout config.Duration + clientReceiveTimeout config.Duration + feeCollectorTokenPublicKey config.String + enableAirdrops config.Bool + enableAsyncAirdropProcessing config.Bool + airdropperOwnerPublicKey config.String + swapSubsidizerOwnerPublicKey config.String + swapPriorityFeeMultiple config.Float64 + stripedLockParallelization config.Uint64 +} + +// ConfigProvider defines how config values are pulled +type ConfigProvider func() *conf + +// WithEnvConfigs returns configuration pulled from environment variables +func WithEnvConfigs() ConfigProvider { + return func() *conf { + return &conf{ + disableSubmitIntent: env.NewBoolConfig(DisableSubmitIntentConfigEnvName, defaultDisableSubmitIntent), + disableAntispamChecks: wrapper.NewBoolConfig(memory.NewConfig(false), false), + disableAmlChecks: wrapper.NewBoolConfig(memory.NewConfig(false), false), + disableBlockchainChecks: env.NewBoolConfig(DisableBlockchainChecksConfigEnvName, defaultDisableBlockchainChecks), + submitIntentTimeout: env.NewDurationConfig(SubmitIntentTimeoutConfigEnvName, defaultSubmitIntentTimeout), + swapTimeout: env.NewDurationConfig(SwapTimeoutConfigEnvName, defaultSwapTimeout), + clientReceiveTimeout: env.NewDurationConfig(ClientReceiveTimeoutConfigEnvName, defaultClientReceiveTimeout), + feeCollectorTokenPublicKey: env.NewStringConfig(FeeCollectorTokenPublicKeyConfigEnvName, defaultFeeCollectorPublicKey), + enableAirdrops: env.NewBoolConfig(EnableAirdropsConfigEnvName, defaultEnableAirdrops), + enableAsyncAirdropProcessing: wrapper.NewBoolConfig(memory.NewConfig(true), true), + airdropperOwnerPublicKey: env.NewStringConfig(AirdropperOwnerPublicKeyEnvName, defaultAirdropperOwnerPublicKey), + swapSubsidizerOwnerPublicKey: env.NewStringConfig(SwapSubsidizerOwnerPublicKeyEnvName, defaultSwapSubsidizerOwnerPublicKey), + swapPriorityFeeMultiple: env.NewFloat64Config(SwapPriorityFeeMultiple, defaultSwapPriorityFeeMultiple), + stripedLockParallelization: wrapper.NewUint64Config(memory.NewConfig(8192), 8192), + } + } +} + +type testOverrides struct { + disableSubmitIntent bool + enableAntispamChecks bool + enableAmlChecks bool + enableAirdrops bool + clientReceiveTimeout time.Duration + feeCollectorTokenPublicKey string +} + +func withManualTestOverrides(overrides *testOverrides) ConfigProvider { + return func() *conf { + return &conf{ + disableSubmitIntent: wrapper.NewBoolConfig(memory.NewConfig(overrides.disableSubmitIntent), defaultDisableSubmitIntent), + disableAntispamChecks: wrapper.NewBoolConfig(memory.NewConfig(!overrides.enableAntispamChecks), false), + disableAmlChecks: wrapper.NewBoolConfig(memory.NewConfig(!overrides.enableAmlChecks), false), + disableBlockchainChecks: wrapper.NewBoolConfig(memory.NewConfig(true), true), + submitIntentTimeout: wrapper.NewDurationConfig(memory.NewConfig(defaultSubmitIntentTimeout), defaultSubmitIntentTimeout), + swapTimeout: wrapper.NewDurationConfig(memory.NewConfig(defaultSwapTimeout), defaultSwapTimeout), + clientReceiveTimeout: wrapper.NewDurationConfig(memory.NewConfig(overrides.clientReceiveTimeout), defaultClientReceiveTimeout), + feeCollectorTokenPublicKey: wrapper.NewStringConfig(memory.NewConfig(overrides.feeCollectorTokenPublicKey), defaultFeeCollectorPublicKey), + enableAirdrops: wrapper.NewBoolConfig(memory.NewConfig(overrides.enableAirdrops), false), + enableAsyncAirdropProcessing: wrapper.NewBoolConfig(memory.NewConfig(false), false), + airdropperOwnerPublicKey: wrapper.NewStringConfig(memory.NewConfig(defaultAirdropperOwnerPublicKey), defaultAirdropperOwnerPublicKey), + swapSubsidizerOwnerPublicKey: wrapper.NewStringConfig(memory.NewConfig(defaultSwapSubsidizerOwnerPublicKey), defaultSwapSubsidizerOwnerPublicKey), + swapPriorityFeeMultiple: wrapper.NewFloat64Config(memory.NewConfig(defaultSwapPriorityFeeMultiple), defaultSwapPriorityFeeMultiple), + stripedLockParallelization: wrapper.NewUint64Config(memory.NewConfig(4), 4), + } + } +} diff --git a/pkg/code/server/grpc/transaction/v2/errors.go b/pkg/code/server/transaction/errors.go similarity index 87% rename from pkg/code/server/grpc/transaction/v2/errors.go rename to pkg/code/server/transaction/errors.go index c6e2aa56..b8270223 100644 --- a/pkg/code/server/grpc/transaction/v2/errors.go +++ b/pkg/code/server/transaction/errors.go @@ -11,7 +11,6 @@ import ( commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - "github.com/code-payments/code-server/pkg/code/antispam" "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/cvm" @@ -70,27 +69,14 @@ func (e IntentValidationError) Error() string { type IntentDeniedError struct { message string - reason antispam.Reason } func newIntentDeniedError(message string) IntentDeniedError { return IntentDeniedError{ message: message, - reason: antispam.ReasonUnspecified, } } -func newIntentDeniedErrorWithAntispamReason(reason antispam.Reason, message string) IntentDeniedError { - return IntentDeniedError{ - message: message, - reason: reason, - } -} - -func newIntentDeniedErrorf(reason antispam.Reason, format string, args ...any) IntentDeniedError { - return newIntentDeniedError(fmt.Sprintf(format, args...)) -} - func (e IntentDeniedError) Error() string { return e.message } @@ -115,13 +101,11 @@ func (e SwapValidationError) Error() string { type SwapDeniedError struct { message string - reason antispam.Reason } func newSwapDeniedError(message string) SwapDeniedError { return SwapDeniedError{ message: message, - reason: antispam.ReasonUnspecified, } } @@ -230,34 +214,10 @@ func toDeniedErrorDetails(err error) *transactionpb.ErrorDetails { reasonString = reasonString[:maxReasonStringLength] } - var antispamReason antispam.Reason - switch typed := err.(type) { - case IntentDeniedError: - antispamReason = typed.reason - case SwapDeniedError: - antispamReason = typed.reason - default: - antispamReason = antispam.ReasonUnspecified - } - - var code transactionpb.DeniedErrorDetails_Code - switch antispamReason { - case antispam.ReasonUnsupportedCountry: - code = transactionpb.DeniedErrorDetails_UNSUPPORTED_COUNTRY - case antispam.ReasonUnsupportedDevice: - code = transactionpb.DeniedErrorDetails_UNSUPPORTED_DEVICE - case antispam.ReasonTooManyFreeAccountsForPhoneNumber: - code = transactionpb.DeniedErrorDetails_TOO_MANY_FREE_ACCOUNTS_FOR_PHONE_NUMBER - case antispam.ReasonTooManyFreeAccountsForDevice: - code = transactionpb.DeniedErrorDetails_TOO_MANY_FREE_ACCOUNTS_FOR_DEVICE - default: - code = transactionpb.DeniedErrorDetails_UNSPECIFIED - } - return &transactionpb.ErrorDetails{ Type: &transactionpb.ErrorDetails_Denied{ Denied: &transactionpb.DeniedErrorDetails{ - Code: code, + Code: transactionpb.DeniedErrorDetails_UNSPECIFIED, Reason: reasonString, }, }, diff --git a/pkg/code/server/grpc/transaction/v2/intent.go b/pkg/code/server/transaction/intent.go similarity index 69% rename from pkg/code/server/grpc/transaction/v2/intent.go rename to pkg/code/server/transaction/intent.go index c569cd12..f8e3661f 100644 --- a/pkg/code/server/grpc/transaction/v2/intent.go +++ b/pkg/code/server/transaction/intent.go @@ -6,12 +6,10 @@ import ( "crypto/ed25519" "database/sql" "encoding/base64" - "encoding/hex" "strings" "time" "github.com/mr-tron/base58/base58" - "github.com/newrelic/go-agent/v3/newrelic" "github.com/pkg/errors" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" @@ -19,25 +17,18 @@ import ( "google.golang.org/protobuf/proto" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" - chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/nonce" "github.com/code-payments/code-server/pkg/code/data/timelock" "github.com/code-payments/code-server/pkg/code/data/webhook" - "github.com/code-payments/code-server/pkg/code/push" "github.com/code-payments/code-server/pkg/code/transaction" "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/solana" "github.com/code-payments/code-server/pkg/solana/cvm" @@ -92,12 +83,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm intentId := base58.Encode(submitActionsReq.Id.Value) log = log.WithField("intent", intentId) - rendezvousKey, err := common.NewAccountFromPublicKeyString(intentId) - if err != nil { - log.WithError(err).Warn("invalid rendezvous key") - return handleSubmitIntentError(streamer, err) - } - marshalled, err := proto.Marshal(submitActionsReq) if err == nil { log = log.WithField("submit_actions_data_dump", base64.URLEncoding.EncodeToString(marshalled)) @@ -105,32 +90,17 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm // Figure out what kind of intent we're operating on and initialize the intent handler var intentHandler interface{} - var intentRequiresNewTreasuryPoolFunds bool switch submitActionsReq.Metadata.Type.(type) { case *transactionpb.Metadata_OpenAccounts: log = log.WithField("intent_type", "open_accounts") - intentHandler = NewOpenAccountsIntentHandler(s.conf, s.data, s.antispamGuard, s.maxmind) - case *transactionpb.Metadata_SendPrivatePayment: - log = log.WithField("intent_type", "send_private_payment") - intentHandler = NewSendPrivatePaymentIntentHandler(s.conf, s.data, s.pusher, s.antispamGuard, s.amlGuard, s.maxmind) - intentRequiresNewTreasuryPoolFunds = true - case *transactionpb.Metadata_ReceivePaymentsPrivately: - log = log.WithField("intent_type", "receive_payments_privately") - intentHandler = NewReceivePaymentsPrivatelyIntentHandler(s.conf, s.data, s.antispamGuard, s.amlGuard) - intentRequiresNewTreasuryPoolFunds = true - case *transactionpb.Metadata_UpgradePrivacy: - log = log.WithField("intent_type", "upgrade_privacy") - intentHandler = NewUpgradePrivacyIntentHandler(s.conf, s.data) + intentHandler = NewOpenAccountsIntentHandler(s.conf, s.data, s.antispamGuard) case *transactionpb.Metadata_SendPublicPayment: log = log.WithField("intent_type", "send_public_payment") - intentHandler = NewSendPublicPaymentIntentHandler(s.conf, s.data, s.pusher, s.antispamGuard, s.maxmind) + intentHandler = NewSendPublicPaymentIntentHandler(s.conf, s.data, s.antispamGuard) case *transactionpb.Metadata_ReceivePaymentsPublicly: - log = log.WithField("intent_type", "receive_payments_publicly") - intentHandler = NewReceivePaymentsPubliclyIntentHandler(s.conf, s.data, s.antispamGuard, s.maxmind) - case *transactionpb.Metadata_EstablishRelationship: - log = log.WithField("intent_type", "establish_relationship") - intentHandler = NewEstablishRelationshipIntentHandler(s.conf, s.data, s.antispamGuard) - + return newIntentDeniedError("remote send requires rewrite") + //log = log.WithField("intent_type", "receive_payments_publicly") + //intentHandler = NewReceivePaymentsPubliclyIntentHandler(s.conf, s.data, s.antispamGuard) default: return handleSubmitIntentError(streamer, status.Error(codes.InvalidArgument, "SubmitIntentRequest.SubmitActions.Metadata is nil")) } @@ -153,13 +123,11 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm // For all allowed cases of owner account types that can call SubmitIntent, // we need to find the phone-verified user's 12 words who initiated the intent. var initiatorOwnerAccount *common.Account - var initiatorPhoneNumber *string submitActionsOwnerMetadata, err := common.GetOwnerMetadata(ctx, s.data, submitActionsOwnerAccount) if err == nil { switch submitActionsOwnerMetadata.Type { case common.OwnerTypeUser12Words: initiatorOwnerAccount = submitActionsOwnerAccount - initiatorPhoneNumber = &submitActionsOwnerMetadata.VerificationRecord.PhoneNumber case common.OwnerTypeRemoteSendGiftCard: // Remote send gift cards can only be the owner of an intent for a // remote send public receive. In this instance, we need to inspect @@ -186,13 +154,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm log.WithError(err).Warn("failure getting user initiator owner account") return handleSubmitIntentError(streamer, err) } - - userOwnerMetadata, err := common.GetOwnerMetadata(ctx, s.data, initiatorOwnerAccount) - if err != nil { - log.WithError(err).Warn("failure getting user initiator owner account") - return handleSubmitIntentError(streamer, err) - } - initiatorPhoneNumber = &userOwnerMetadata.VerificationRecord.PhoneNumber default: return newActionValidationError(submitActionsReq.Actions[0], "expected a no privacy withdraw action") } @@ -210,12 +171,11 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm } // All intents must be initiated by a phone-verified user - if initiatorOwnerAccount == nil || initiatorPhoneNumber == nil { + if initiatorOwnerAccount == nil { log.Info("intent not initiated by phone-verified user 12 words") return handleSubmitIntentError(streamer, ErrNotPhoneVerified) } log = log.WithField("initiator_owner_account", initiatorOwnerAccount.PublicKey().ToBase58()) - log = log.WithField("initiator_phone_number", *initiatorPhoneNumber) // Check that all provided signatures in proto messages are valid signature := submitActionsReq.Signature @@ -258,7 +218,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm intentRecord := &intent.Record{ IntentId: intentId, InitiatorOwnerAccount: initiatorOwnerAccount.PublicKey().ToBase58(), - InitiatorPhoneNumber: initiatorPhoneNumber, State: intent.StateUnknown, CreatedAt: time.Now(), } @@ -267,15 +226,9 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm // requirements may not be known until populating intent metadata. intentLock := s.intentLocks.Get([]byte(intentId)) initiatorOwnerLock := s.ownerLocks.Get(initiatorOwnerAccount.PublicKey().ToBytes()) - phoneLock := s.phoneLocks.Get([]byte(*initiatorPhoneNumber)) intentLock.Lock() initiatorOwnerLock.Lock() - phoneLock.Lock() - var phoneLockUnlocked bool // Can be unlocked earlier than RPC end defer func() { - if !phoneLockUnlocked { - phoneLock.Unlock() - } initiatorOwnerLock.Unlock() intentLock.Unlock() }() @@ -365,13 +318,8 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm defer giftCardLock.Unlock() } - var deviceToken *string - if submitActionsReq.DeviceToken != nil { - deviceToken = &submitActionsReq.DeviceToken.Value - } - // Validate the new intent with intent-specific logic - err = createIntentHandler.AllowCreation(ctx, intentRecord, submitActionsReq.Metadata, submitActionsReq.Actions, deviceToken) + err = createIntentHandler.AllowCreation(ctx, intentRecord, submitActionsReq.Metadata, submitActionsReq.Actions) if err != nil { switch err.(type) { case IntentValidationError: @@ -385,23 +333,8 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm } return handleSubmitIntentError(streamer, err) } - - // Generically handle treasury pool status to protect ourselves against - // overly large usage (ie. multiples of the total available funds) - if intentRequiresNewTreasuryPoolFunds { - areTreasuryPoolsAvailable, err := s.areAllTreasuryPoolsAvailable(ctx) - if err != nil { - return handleSubmitIntentError(streamer, err) - } else if !areTreasuryPoolsAvailable { - return status.Error(codes.Unavailable, "temporarily unavailable") - } - } } - // Remove the phone lock early, since we only require it for the Allow methods. - phoneLock.Unlock() - phoneLockUnlocked = true - type fulfillmentWithSigningMetadata struct { record *fulfillment.Record @@ -440,33 +373,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm log = log.WithField("action_type", "no_privacy_withdraw") actionType = action.NoPrivacyWithdraw actionHandler, err = NewNoPrivacyWithdrawActionHandler(intentRecord, typed.NoPrivacyWithdraw) - case *transactionpb.Action_TemporaryPrivacyTransfer: - log = log.WithField("action_type", "temporary_privacy_transfer") - actionType = action.PrivateTransfer - actionHandler, err = NewTemporaryPrivacyTransferActionHandler(ctx, s.conf, s.data, intentRecord, protoAction, false, s.selectTreasuryPoolForAdvance) - case *transactionpb.Action_TemporaryPrivacyExchange: - log = log.WithField("action_type", "temporary_privacy_exchange") - actionType = action.PrivateTransfer - actionHandler, err = NewTemporaryPrivacyTransferActionHandler(ctx, s.conf, s.data, intentRecord, protoAction, true, s.selectTreasuryPoolForAdvance) - case *transactionpb.Action_PermanentPrivacyUpgrade: - log = log.WithField("action_type", "permanent_privacy_upgrade") - actionType = action.PrivateTransfer - - // Pass along the privacy upgrade target found during intent validation - // to avoid duplication of work. - cachedUpgradeTarget, ok := intentHandler.(*UpgradePrivacyIntentHandler).GetCachedUpgradeTarget(typed.PermanentPrivacyUpgrade) - if !ok { - log.Warn("cached privacy upgrade target not found") - return handleSubmitIntentError(streamer, errors.New("cached privacy upgrade target not found")) - } - - actionHandler, err = NewPermanentPrivacyUpgradeActionHandler( - ctx, - s.data, - intentRecord, - typed.PermanentPrivacyUpgrade, - cachedUpgradeTarget, - ) default: return handleSubmitIntentError(streamer, status.Errorf(codes.InvalidArgument, "SubmitIntentRequest.SubmitActions.Actions[%d].Type is nil", i)) } @@ -500,8 +406,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm ActionId: protoAction.Id, ActionType: actionType, - InitiatorPhoneNumber: intentRecord.InitiatorPhoneNumber, - State: action.StateUnknown, } @@ -617,8 +521,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm DisableActiveScheduling: newFulfillmentMetadata.disableActiveScheduling, - InitiatorPhoneNumber: intentRecord.InitiatorPhoneNumber, - State: fulfillment.StateUnknown, } if newFulfillmentMetadata.destination != nil { @@ -736,8 +638,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm } } - var chatMessagesToPush []*chat_util.MessageWithOwner - // Save all of the required DB records in one transaction to complete the // intent operation. It's very bad if we end up failing halfway through. // @@ -808,27 +708,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm return err } - // Update various chats with exchange data messages - err = chat_util.SendCashTransactionsExchangeMessage(ctx, s.data, intentRecord) - if err != nil { - log.WithError(err).Warn("failure updating cash transaction chat") - return err - } - - tipMessagesToPush, err := chat_util.SendTipsExchangeMessage(ctx, s.data, intentRecord) - if err != nil { - log.WithError(err).Warn("failure updating tips chat") - return err - } - chatMessagesToPush = append(chatMessagesToPush, tipMessagesToPush...) - - merchantMessagesToPush, err := chat_util.SendMerchantExchangeMessage(ctx, s.data, intentRecord, actionRecords) - if err != nil { - log.WithError(err).Warn("failure updating merchant chat") - return err - } - chatMessagesToPush = append(chatMessagesToPush, merchantMessagesToPush...) - // Mark the intent as pending once everything else has succeeded err = s.markIntentAsPending(ctx, intentRecord) if err != nil { @@ -837,11 +716,13 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm } // Mark the associated webhook as pending, if it was registered - err = s.markWebhookAsPending(ctx, intentRecord.IntentId) - if err != nil { - log.WithError(err).Warn("failure marking webhook as pending") - return err - } + /* + err = s.markWebhookAsPending(ctx, intentRecord.IntentId) + if err != nil { + log.WithError(err).Warn("failure marking webhook as pending") + return err + } + */ // Create a message on the intent ID to indicate the intent was submitted // @@ -850,18 +731,20 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm // // todo: We could also make this an account update event by creating the message // on each involved owner accounts' stream. - _, err = s.messagingClient.InternallyCreateMessage(ctx, rendezvousKey, &messagingpb.Message{ - Kind: &messagingpb.Message_IntentSubmitted{ - IntentSubmitted: &messagingpb.IntentSubmitted{ - IntentId: submitActionsReq.Id, - Metadata: submitActionsReq.Metadata, + /* + _, err = s.messagingClient.InternallyCreateMessage(ctx, rendezvousKey, &messagingpb.Message{ + Kind: &messagingpb.Message_IntentSubmitted{ + IntentSubmitted: &messagingpb.IntentSubmitted{ + IntentId: submitActionsReq.Id, + Metadata: submitActionsReq.Metadata, + }, }, - }, - }) - if err != nil { - log.WithError(err).Warn("failure creating intent submitted message") - return err - } + }) + if err != nil { + log.WithError(err).Warn("failure creating intent submitted message") + return err + } + */ } return nil @@ -884,31 +767,12 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm if err != nil { log.WithError(err).Warn("failure executing intent committed callback handler handler") } - - if len(chatMessagesToPush) > 0 { - go func() { - for _, chatMessageToPush := range chatMessagesToPush { - push.SendChatMessagePushNotification( - context.TODO(), - s.data, - s.pusher, - chatMessageToPush.Title, - chatMessageToPush.Owner, - chatMessageToPush.Message, - ) - } - }() - } } // Fire off some success metrics if !isIntentUpdateOperation { recordUserIntentCreatedEvent(ctx, intentRecord) } - switch submitActionsReq.Metadata.Type.(type) { - case *transactionpb.Metadata_UpgradePrivacy: - recordPrivacyUpgradedEvent(ctx, intentRecord, len(submitActionsReq.Actions)) - } latencyAfterSignatureSubmission := time.Since(tsAfterSignatureSubmission) recordSubmitIntentLatencyBreakdownEvent( @@ -926,17 +790,6 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm metricsIntentTypeValue, ) - // There are no intent-based airdrops ATM - if false { - backgroundCtx := context.Background() - - // todo: generic metrics utility for this - nr, ok := ctx.Value(metrics.NewRelicContextKey).(*newrelic.Application) - if ok { - backgroundCtx = context.WithValue(backgroundCtx, metrics.NewRelicContextKey, nr) - } - } - // RPC is finished. Send success to the client if err := streamer.Send(okResp); err != nil { return handleSubmitIntentError(streamer, err) @@ -1037,8 +890,8 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact var destinationOwnerAccount string switch intentRecord.IntentType { - case intent.SendPrivatePayment: - destinationOwnerAccount = intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount + case intent.SendPublicPayment: + destinationOwnerAccount = intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount } if req.Owner != nil { @@ -1058,44 +911,7 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact OpenAccounts: &transactionpb.OpenAccountsMetadata{}, }, } - case intent.SendPrivatePayment: - destinationAccount, err := common.NewAccountFromPublicKeyString(intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount) - if err != nil { - log.WithError(err).Warn("invalid destination account") - return nil, status.Error(codes.Internal, "") - } - - metadata = &transactionpb.Metadata{ - Type: &transactionpb.Metadata_SendPrivatePayment{ - SendPrivatePayment: &transactionpb.SendPrivatePaymentMetadata{ - Destination: destinationAccount.ToProto(), - ExchangeData: &transactionpb.ExchangeData{ - Currency: strings.ToLower(string(intentRecord.SendPrivatePaymentMetadata.ExchangeCurrency)), - ExchangeRate: intentRecord.SendPrivatePaymentMetadata.ExchangeRate, - NativeAmount: intentRecord.SendPrivatePaymentMetadata.ExchangeRate * float64(kin.FromQuarks(intentRecord.SendPrivatePaymentMetadata.Quantity)), - Quarks: intentRecord.SendPrivatePaymentMetadata.Quantity, - }, - IsWithdrawal: intentRecord.SendPrivatePaymentMetadata.IsWithdrawal, - IsRemoteSend: intentRecord.SendPrivatePaymentMetadata.IsRemoteSend, - }, - }, - } - case intent.ReceivePaymentsPrivately: - sourceAccount, err := common.NewAccountFromPublicKeyString(intentRecord.ReceivePaymentsPrivatelyMetadata.Source) - if err != nil { - log.WithError(err).Warn("invalid source account") - return nil, status.Error(codes.Internal, "") - } - metadata = &transactionpb.Metadata{ - Type: &transactionpb.Metadata_ReceivePaymentsPrivately{ - ReceivePaymentsPrivately: &transactionpb.ReceivePaymentsPrivatelyMetadata{ - Source: sourceAccount.ToProto(), - Quarks: intentRecord.ReceivePaymentsPrivatelyMetadata.Quantity, - IsDeposit: intentRecord.ReceivePaymentsPrivatelyMetadata.IsDeposit, - }, - }, - } case intent.SendPublicPayment: destinationAccount, err := common.NewAccountFromPublicKeyString(intentRecord.SendPublicPaymentMetadata.DestinationTokenAccount) if err != nil { @@ -1110,7 +926,7 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact ExchangeData: &transactionpb.ExchangeData{ Currency: strings.ToLower(string(intentRecord.SendPublicPaymentMetadata.ExchangeCurrency)), ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate, - NativeAmount: intentRecord.SendPublicPaymentMetadata.ExchangeRate * float64(kin.FromQuarks(intentRecord.SendPublicPaymentMetadata.Quantity)), + NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount, Quarks: intentRecord.SendPublicPaymentMetadata.Quantity, }, IsWithdrawal: intentRecord.SendPublicPaymentMetadata.IsWithdrawal, @@ -1219,10 +1035,10 @@ func (s *transactionServer) CanWithdrawToAccount(ctx context.Context, req *trans } // - // Part 4: Is this an owner account with an opened Kin ATA? If so, allow it. + // Part 4: Is this an owner account with an opened Core Mint ATA? If so, allow it. // - ata, err := accountToCheck.ToAssociatedTokenAccount(common.KinMintAccount) + ata, err := accountToCheck.ToAssociatedTokenAccount(common.CoreMintAccount) if err != nil { log.WithError(err).Warn("failure getting ata address") return nil, status.Error(codes.Internal, "") @@ -1253,214 +1069,3 @@ func (s *transactionServer) CanWithdrawToAccount(ctx context.Context, req *trans RequiresInitialization: requiresInitialization, }, nil } - -func (s *transactionServer) GetPrivacyUpgradeStatus(ctx context.Context, req *transactionpb.GetPrivacyUpgradeStatusRequest) (*transactionpb.GetPrivacyUpgradeStatusResponse, error) { - intentId := base58.Encode(req.IntentId.Value) - - log := s.log.WithFields(logrus.Fields{ - "method": "GetPrivacyUpgradeStatus", - "intent": intentId, - "action": req.ActionId, - }) - log = client.InjectLoggingMetadata(ctx, log) - - var result transactionpb.GetPrivacyUpgradeStatusResponse_Result - var upgradeStatus transactionpb.GetPrivacyUpgradeStatusResponse_Status - - // The status is directly tied to our ability to select another commitment - // account to redirect the repayment to - _, err := selectCandidateForPrivacyUpgrade(ctx, s.data, intentId, req.ActionId) - switch err { - case intent.ErrIntentNotFound: - result = transactionpb.GetPrivacyUpgradeStatusResponse_INTENT_NOT_FOUND - case action.ErrActionNotFound: - result = transactionpb.GetPrivacyUpgradeStatusResponse_ACTION_NOT_FOUND - case ErrInvalidActionToUpgrade: - result = transactionpb.GetPrivacyUpgradeStatusResponse_INVALID_ACTION - case ErrPrivacyUpgradeMissed: - upgradeStatus = transactionpb.GetPrivacyUpgradeStatusResponse_TEMPORARY_ACTION_FINALIZED - case ErrPrivacyAlreadyUpgraded: - upgradeStatus = transactionpb.GetPrivacyUpgradeStatusResponse_ALREADY_UPGRADED - case ErrWaitForNextBlock: - upgradeStatus = transactionpb.GetPrivacyUpgradeStatusResponse_WAITING_FOR_NEXT_BLOCK - case nil: - upgradeStatus = transactionpb.GetPrivacyUpgradeStatusResponse_READY_FOR_UPGRADE - default: - log.WithError(err).Warn("failure trying to select candidate for privacy upgrade") - return nil, status.Error(codes.Internal, "") - } - - return &transactionpb.GetPrivacyUpgradeStatusResponse{ - Result: result, - Status: upgradeStatus, - }, nil -} - -// todo: This doesn't prioritize anything right now, and that's probably ok anyways. -func (s *transactionServer) GetPrioritizedIntentsForPrivacyUpgrade(ctx context.Context, req *transactionpb.GetPrioritizedIntentsForPrivacyUpgradeRequest) (*transactionpb.GetPrioritizedIntentsForPrivacyUpgradeResponse, error) { - log := s.log.WithField("method", "GetPrioritizedIntentsForPrivacyUpgrade") - log = client.InjectLoggingMetadata(ctx, log) - - owner, err := common.NewAccountFromProto(req.Owner) - if err != nil { - log.WithError(err).Warn("invalid owner account") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err - } - - // Get a set of commitment records that look like they can be upgraded. - // We simply pick the first limit number of commitments by an owner. - // Earlier commitments have a better chance of having an available merkle - // proof. - // - // Get a small number of payments worth of commitments. A payment in the - // worst case has ~15 actions for a send or receive. Since a client has - // limited time to upgrade, it makes sense to not delay by processing too - // many commitments. - commitmentRecords, err := s.data.GetUpgradeableCommitmentsByOwner(ctx, owner.PublicKey().ToBase58(), 15) - if err == commitment.ErrCommitmentNotFound { - return &transactionpb.GetPrioritizedIntentsForPrivacyUpgradeResponse{ - Result: transactionpb.GetPrioritizedIntentsForPrivacyUpgradeResponse_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting commitment records") - return nil, status.Error(codes.Internal, "") - } - - // Filter out commitment records that don't have a merkle proof. These - // can't be upgraded right now. - var commitmentRecordsWithProofs []*commitment.Record - for _, commitmentRecord := range commitmentRecords { - log := log.WithField("commitment", commitmentRecord.Address) - - canUpgrade, err := canUpgradeCommitmentAction(ctx, s.data, commitmentRecord) - if err != nil { - log.WithError(err).Warn("failure checking if commitment action can be upgraded") - return nil, status.Error(codes.Internal, "") - } else if canUpgrade { - commitmentRecordsWithProofs = append(commitmentRecordsWithProofs, commitmentRecord) - } - } - - // There's nothing to upgrade right now - if len(commitmentRecordsWithProofs) == 0 { - return &transactionpb.GetPrioritizedIntentsForPrivacyUpgradeResponse{ - Result: transactionpb.GetPrioritizedIntentsForPrivacyUpgradeResponse_NOT_FOUND, - }, nil - } - - limit := int(req.Limit) - if limit == 0 { - limit = 10 - } - - commitmentsByIntent := make(map[string][]*commitment.Record) - for _, commitmentRecord := range commitmentRecordsWithProofs { - // Keep within limits - _, exists := commitmentsByIntent[commitmentRecord.Intent] - if !exists && len(commitmentsByIntent) >= limit { - continue - } - - commitmentsByIntent[commitmentRecord.Intent] = append(commitmentsByIntent[commitmentRecord.Intent], commitmentRecord) - } - - // Convert to proto models - var items []*transactionpb.UpgradeableIntent - for intentId, commitmentRecords := range commitmentsByIntent { - log := log.WithField("intent", intentId) - - item, err := toUpgradeableIntentProto(ctx, s.data, intentId, commitmentRecords) - if err != nil { - log.WithError(err).Warn("failure converting commitment records to an upgradeable intent proto message") - return nil, status.Error(codes.Internal, "") - } - - items = append(items, item) - } - - return &transactionpb.GetPrioritizedIntentsForPrivacyUpgradeResponse{ - Result: transactionpb.GetPrioritizedIntentsForPrivacyUpgradeResponse_OK, - Items: items, - }, nil -} - -func toUpgradeableIntentProto(ctx context.Context, data code_data.Provider, intentId string, commitmentRecords []*commitment.Record) (*transactionpb.UpgradeableIntent, error) { - intentIDBytes, err := base58.Decode(intentId) - if err != nil { - return nil, err - } - - var actions []*transactionpb.UpgradeableIntent_UpgradeablePrivateAction - for _, commitmentRecord := range commitmentRecords { - if commitmentRecord.Intent != intentId { - return nil, errors.New("commitment intent id doesn't match") - } - - fulfillmentRecords, err := data.GetAllFulfillmentsByTypeAndAction(ctx, fulfillment.TemporaryPrivacyTransferWithAuthority, commitmentRecord.Intent, commitmentRecord.ActionId) - if err != nil { - return nil, err - } - - if len(fulfillmentRecords) != 1 || *fulfillmentRecords[0].Destination != commitmentRecord.VaultAddress { - return nil, errors.New("fulfillment to upgrade was not found") - } - fulfillmentToUpgrade := fulfillmentRecords[0] - - // todo: this can be heavily cached - sourceAccountInfo, err := data.GetAccountInfoByTokenAddress(ctx, fulfillmentToUpgrade.Source) - if err != nil { - return nil, err - } - - originalDestination, err := common.NewAccountFromPublicKeyString(commitmentRecord.Destination) - if err != nil { - return nil, err - } - - treasuryPool, err := common.NewAccountFromPublicKeyString(commitmentRecord.Pool) - if err != nil { - return nil, err - } - - recentRootBytes, err := hex.DecodeString(commitmentRecord.RecentRoot) - if err != nil { - return nil, err - } - - clientSignature, err := base58.Decode(*fulfillmentToUpgrade.VirtualSignature) - if err != nil { - return nil, err - } - - action := &transactionpb.UpgradeableIntent_UpgradeablePrivateAction{ - TransactionBlob: nil, // todo: need a solution or can we get rid of this? - ClientSignature: &commonpb.Signature{ - Value: clientSignature, - }, - ActionId: commitmentRecord.ActionId, - SourceAccountType: sourceAccountInfo.AccountType, - SourceDerivationIndex: sourceAccountInfo.Index, - OriginalDestination: originalDestination.ToProto(), - OriginalAmount: commitmentRecord.Amount, - Treasury: treasuryPool.ToProto(), - RecentRoot: &commonpb.Hash{ - Value: recentRootBytes, - }, - } - actions = append(actions, action) - } - - return &transactionpb.UpgradeableIntent{ - Id: &commonpb.IntentId{ - Value: intentIDBytes, - }, - Actions: actions, - }, nil -} diff --git a/pkg/code/server/transaction/intent_handler.go b/pkg/code/server/transaction/intent_handler.go new file mode 100644 index 00000000..68bb5432 --- /dev/null +++ b/pkg/code/server/transaction/intent_handler.go @@ -0,0 +1,1428 @@ +package transaction_v2 + +import ( + "bytes" + "context" + "math" + "strings" + "time" + + "github.com/mr-tron/base58/base58" + "github.com/pkg/errors" + + commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" + transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" + + "github.com/code-payments/code-server/pkg/code/antispam" + "github.com/code-payments/code-server/pkg/code/balance" + "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/code/data/account" + "github.com/code-payments/code-server/pkg/code/data/action" + "github.com/code-payments/code-server/pkg/code/data/intent" + "github.com/code-payments/code-server/pkg/code/data/paymentrequest" + exchange_rate_util "github.com/code-payments/code-server/pkg/code/exchangerate" + currency_lib "github.com/code-payments/code-server/pkg/currency" + "github.com/code-payments/code-server/pkg/solana" +) + +var accountTypesToOpen = []commonpb.AccountType{ + commonpb.AccountType_PRIMARY, +} + +type lockableAccounts struct { + DestinationOwner *common.Account + RemoteSendGiftCardVault *common.Account +} + +// CreateIntentHandler is an interface for handling new intent creations +type CreateIntentHandler interface { + // PopulateMetadata adds intent metadata to the provided intent record + // using the client-provided protobuf variant. No other fields in the + // intent should be modified. + PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error + + // IsNoop determines whether the intent is a no-op operation. SubmitIntent will + // simply return OK and stop any further intent processing. + // + // Note: This occurs before validation, so if anything appears out-of-order, then + // the recommendation is to return false and have the verification logic catch the + // error. + IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) + + // GetAdditionalAccountsToLock gets additional accounts to apply distributed + // locking on that are specific to an intent. + // + // Note: Assumes relevant information is contained in the intent record after + // calling PopulateMetadata. + GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) + + // AllowCreation determines whether the new intent creation should be allowed. + AllowCreation(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) error + + // OnSaveToDB is a callback when the intent is being saved to the DB + // within the scope of a DB transaction. Additional supporting DB records + // (ie. not the intent record) relevant to the intent should be saved here. + OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error + + // OnCommittedToDB is a callback when the intent has been committed to the + // DB. Any instant side-effects should called here, and can be done async + // in a new goroutine to not affect SubmitIntent latency. + // + // Note: Any errors generated here have no effect on rolling back the intent. + // This is all best-effort up to this point. Use a worker for things + // requiring retries! + OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error +} + +// UpdateIntentHandler is an interface for handling updates to an existing intent +type UpdateIntentHandler interface { + // AllowUpdate determines whether an intent update should be allowed. + AllowUpdate(ctx context.Context, existingIntent *intent.Record, metdata *transactionpb.Metadata, actions []*transactionpb.Action) error +} + +type OpenAccountsIntentHandler struct { + conf *conf + data code_data.Provider + antispamGuard *antispam.Guard +} + +func NewOpenAccountsIntentHandler(conf *conf, data code_data.Provider, antispamGuard *antispam.Guard) CreateIntentHandler { + return &OpenAccountsIntentHandler{ + conf: conf, + data: data, + antispamGuard: antispamGuard, + } +} + +func (h *OpenAccountsIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { + typedProtoMetadata := protoMetadata.GetOpenAccounts() + if typedProtoMetadata == nil { + return errors.New("unexpected metadata proto message") + } + + intentRecord.IntentType = intent.OpenAccounts + intentRecord.OpenAccountsMetadata = &intent.OpenAccountsMetadata{} + + return nil +} + +func (h *OpenAccountsIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { + initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) + if err != nil { + return false, err + } + + _, err = h.data.GetLatestIntentByInitiatorAndType(ctx, intent.OpenAccounts, initiatiorOwnerAccount.PublicKey().ToBase58()) + if err == nil { + return true, nil + } else if err != intent.ErrIntentNotFound { + return false, err + } + + return false, nil +} + +func (h *OpenAccountsIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { + return &lockableAccounts{}, nil +} + +func (h *OpenAccountsIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) error { + typedMetadata := metadata.GetOpenAccounts() + if typedMetadata == nil { + return errors.New("unexpected metadata proto message") + } + + initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) + if err != nil { + return err + } + + // + // Part 1: Intent ID validation + // + + err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) + if err != nil { + return err + } + + // + // Part 2: Antispam checks against the phone number + // + + if !h.conf.disableAntispamChecks.Get(ctx) { + allow, err := h.antispamGuard.AllowOpenAccounts(ctx, initiatiorOwnerAccount) + if err != nil { + return err + } else if !allow { + return newIntentDeniedError("antispam guard denied account creation") + } + } + + // + // Part 3: Validate the owner hasn't already created an OpenAccounts intent + // + + _, err = h.data.GetLatestIntentByInitiatorAndType(ctx, intent.OpenAccounts, initiatiorOwnerAccount.PublicKey().ToBase58()) + if err == nil { + return newStaleStateError("already submitted intent to open accounts") + } else if err != intent.ErrIntentNotFound { + return err + } + + // + // Part 4: Validate the individual actions + // + + err = h.validateActions(ctx, initiatiorOwnerAccount, actions) + if err != nil { + return err + } + + // + // Part 5: Local simulation + // + + simResult, err := LocalSimulation(ctx, h.data, actions) + if err != nil { + return err + } + + // + // Part 6: Validate fee payments + // + + return validateFeePayments(ctx, h.data, intentRecord, simResult) +} + +func (h *OpenAccountsIntentHandler) validateActions(ctx context.Context, initiatiorOwnerAccount *common.Account, actions []*transactionpb.Action) error { + expectedActionCount := len(accountTypesToOpen) + if len(actions) != expectedActionCount { + return newIntentValidationErrorf("expected %d total actions", expectedActionCount) + } + + for i, expectedAccountType := range accountTypesToOpen { + openAction := actions[i] + + if openAction.GetOpenAccount() == nil { + return newActionValidationError(openAction, "expected an open account action") + } + + if openAction.GetOpenAccount().AccountType != expectedAccountType { + return newActionValidationErrorf(openAction, "account type must be %s", expectedAccountType) + } + + if openAction.GetOpenAccount().Index != 0 { + return newActionValidationError(openAction, "index must be 0 for all newly opened accounts") + } + + if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatiorOwnerAccount.PublicKey().ToBytes()) { + return newActionValidationErrorf(openAction, "owner must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) + } + + switch expectedAccountType { + case commonpb.AccountType_PRIMARY: + if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { + return newActionValidationErrorf(openAction, "authority must be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) + } + default: + if bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { + return newActionValidationErrorf(openAction, "authority cannot be %s", initiatiorOwnerAccount.PublicKey().ToBase58()) + } + } + + expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) + if err != nil { + return err + } + + if !bytes.Equal(openAction.GetOpenAccount().Token.Value, expectedVaultAccount.PublicKey().ToBytes()) { + return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) + } + + if err := validateTimelockUnlockStateDoesntExist(ctx, h.data, openAction.GetOpenAccount()); err != nil { + return err + } + } + + return nil +} + +func (h *OpenAccountsIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { + return nil +} + +func (h *OpenAccountsIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { + return nil +} + +type SendPublicPaymentIntentHandler struct { + conf *conf + data code_data.Provider + antispamGuard *antispam.Guard +} + +func NewSendPublicPaymentIntentHandler( + conf *conf, + data code_data.Provider, + antispamGuard *antispam.Guard, +) CreateIntentHandler { + return &SendPublicPaymentIntentHandler{ + conf: conf, + data: data, + antispamGuard: antispamGuard, + } +} + +func (h *SendPublicPaymentIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { + typedProtoMetadata := protoMetadata.GetSendPublicPayment() + if typedProtoMetadata == nil { + return errors.New("unexpected metadata proto message") + } + + exchangeData := typedProtoMetadata.ExchangeData + + usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) + if err != nil { + return errors.Wrap(err, "error getting current usd exchange rate") + } + + destination, err := common.NewAccountFromProto(typedProtoMetadata.Destination) + if err != nil { + return err + } + + destinationAccountInfo, err := h.data.GetAccountInfoByTokenAddress(ctx, destination.PublicKey().ToBase58()) + if err != nil && err != account.ErrAccountInfoNotFound { + return err + } + + intentRecord.IntentType = intent.SendPublicPayment + intentRecord.SendPublicPaymentMetadata = &intent.SendPublicPaymentMetadata{ + DestinationTokenAccount: destination.PublicKey().ToBase58(), + Quantity: exchangeData.Quarks, + + ExchangeCurrency: currency_lib.Code(exchangeData.Currency), + ExchangeRate: exchangeData.ExchangeRate, + NativeAmount: typedProtoMetadata.ExchangeData.NativeAmount, + UsdMarketValue: usdExchangeRecord.Rate * float64(exchangeData.Quarks) / float64(common.CoreMintQuarksPerUnit), + + IsWithdrawal: typedProtoMetadata.IsWithdrawal, + } + + if destinationAccountInfo != nil { + intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount = destinationAccountInfo.OwnerAccount + } + + return nil +} + +func (h *SendPublicPaymentIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { + return false, nil +} + +func (h *SendPublicPaymentIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { + if len(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) == 0 { + return &lockableAccounts{}, nil + } + + destinationOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) + if err != nil { + return nil, err + } + + return &lockableAccounts{ + DestinationOwner: destinationOwnerAccount, + }, nil +} + +func (h *SendPublicPaymentIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action) error { + typedMetadata := untypedMetadata.GetSendPublicPayment() + if typedMetadata == nil { + return errors.New("unexpected metadata proto message") + } + + initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) + if err != nil { + return err + } + + initiatorAccountsByType, err := common.GetLatestCodeTimelockAccountRecordsForOwner(ctx, h.data, initiatiorOwnerAccount) + if err != nil { + return err + } + + initiatorAccounts := make([]*common.AccountRecords, 0) + initiatorAccountsByVault := make(map[string]*common.AccountRecords) + for _, batchRecords := range initiatorAccountsByType { + for _, records := range batchRecords { + initiatorAccounts = append(initiatorAccounts, records) + initiatorAccountsByVault[records.General.TokenAccount] = records + } + } + + // + // Part 1: Intent ID validation + // + + err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) + if err != nil { + return err + } + + // + // Part 2: Antispam guard checks against the phone number + // + + if !h.conf.disableAntispamChecks.Get(ctx) { + destination, err := common.NewAccountFromProto(typedMetadata.Destination) + if err != nil { + return err + } + + allow, err := h.antispamGuard.AllowSendPayment(ctx, initiatiorOwnerAccount, true, destination) + if err != nil { + return err + } else if !allow { + return ErrTooManyPayments + } + } + + // + // Part 3: Account validation to determine if it's managed by Code + // + + err = validateAllUserAccountsManagedByCode(ctx, initiatorAccounts) + if err != nil { + return err + } + + // + // Part 4: Exchange data validation + // + + if err := validateExchangeDataWithinIntent(ctx, h.data, intentRecord.IntentId, typedMetadata.ExchangeData); err != nil { + return err + } + + // + // Part 5: Local simulation + // + + simResult, err := LocalSimulation(ctx, h.data, actions) + if err != nil { + return err + } + + // + // Part 6: Validate fee payments + // + + err = validateFeePayments(ctx, h.data, intentRecord, simResult) + if err != nil { + return err + } + + // + // Part 7: Validate the individual actions + // + + return h.validateActions( + ctx, + initiatiorOwnerAccount, + initiatorAccountsByType, + initiatorAccountsByVault, + intentRecord, + typedMetadata, + actions, + simResult, + ) +} + +func (h *SendPublicPaymentIntentHandler) validateActions( + ctx context.Context, + initiatorOwnerAccount *common.Account, + initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, + initiatorAccountsByVault map[string]*common.AccountRecords, + intentRecord *intent.Record, + metadata *transactionpb.SendPublicPaymentMetadata, + actions []*transactionpb.Action, + simResult *LocalSimulationResult, +) error { + if len(actions) != 1 { + return newIntentValidationError("expected 1 action") + } + + var source *common.Account + var err error + if metadata.Source != nil { + source, err = common.NewAccountFromProto(metadata.Source) + if err != nil { + return err + } + } else { + // Backwards compat for old clients using metadata without source. It was + // always assumed to be from the primary account + source, err = common.NewAccountFromPublicKeyString(initiatorAccountsByType[commonpb.AccountType_PRIMARY][0].General.TokenAccount) + if err != nil { + return err + } + } + + destination, err := common.NewAccountFromProto(metadata.Destination) + if err != nil { + return err + } + + // Part 1: Check the source and destination accounts are valid + + destinationAccountInfo, err := h.data.GetAccountInfoByTokenAddress(ctx, destination.PublicKey().ToBase58()) + switch err { + case nil: + // Code->Code public withdraws must be done against other deposit accounts + if metadata.IsWithdrawal && destinationAccountInfo.AccountType != commonpb.AccountType_PRIMARY { + return newIntentValidationError("destination account must be a deposit account") + } + + // And the destination cannot be the source of funds, since that results in a no-op + if source.PublicKey().ToBase58() == destinationAccountInfo.TokenAccount { + return newIntentValidationError("payment is a no-op") + } + case account.ErrAccountInfoNotFound: + // Check whether the destination account is a Kin token account that's + // been created on the blockchain. + if !h.conf.disableBlockchainChecks.Get(ctx) { + err = validateExternalTokenAccountWithinIntent(ctx, h.data, destination) + if err != nil { + return err + } + } + default: + return err + } + + sourceAccountRecords, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] + if !ok || sourceAccountRecords.General.AccountType != commonpb.AccountType_PRIMARY { + return newIntentValidationError("source account must be a deposit account") + } + + // + // Part 2: Validate actions match intent metadata + // + + // + // Part 2.1: Check destination account is paid exact quark amount from the deposit account + // + + destinationSimulation, ok := simResult.SimulationsByAccount[destination.PublicKey().ToBase58()] + if !ok { + return newIntentValidationErrorf("must send payment to destination account %s", destination.PublicKey().ToBase58()) + } else if destinationSimulation.Transfers[0].IsPrivate || destinationSimulation.Transfers[0].IsWithdraw { + return newActionValidationError(destinationSimulation.Transfers[0].Action, "payment sent to destination must be a public transfer") + } else if destinationSimulation.GetDeltaQuarks() != int64(metadata.ExchangeData.Quarks) { + return newActionValidationErrorf(destinationSimulation.Transfers[0].Action, "must send %d quarks to destination account", metadata.ExchangeData.Quarks) + } + + // + // Part 2.2: Check that the user's deposit account was used as the source of funds + // as specified in the metadata + // + + sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] + if !ok { + return newIntentValidationErrorf("must send payment from source account %s", source.PublicKey().ToBase58()) + } else if sourceSimulation.GetDeltaQuarks() != -int64(metadata.ExchangeData.Quarks) { + return newActionValidationErrorf(sourceSimulation.Transfers[0].Action, "must send %d quarks from source account", metadata.ExchangeData.Quarks) + } + + // Part 3: Generic validation of actions that move money + + err = validateMoneyMovementActionUserAccounts(intent.SendPublicPayment, initiatorAccountsByVault, actions) + if err != nil { + return err + } + + // Part 4: Sanity check no open and closed accounts + + if len(simResult.GetOpenedAccounts()) > 0 { + return newIntentValidationError("cannot open any account") + } + + if len(simResult.GetClosedAccounts()) > 0 { + return newIntentValidationError("cannot close any account") + } + + return nil +} + +func (h *SendPublicPaymentIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { + return nil +} + +func (h *SendPublicPaymentIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { + return nil +} + +// Generally needs a rewrite to send funds to the primary account +type ReceivePaymentsPubliclyIntentHandler struct { + conf *conf + data code_data.Provider + antispamGuard *antispam.Guard + + cachedGiftCardIssuedIntentRecord *intent.Record +} + +func NewReceivePaymentsPubliclyIntentHandler(conf *conf, data code_data.Provider, antispamGuard *antispam.Guard) CreateIntentHandler { + return &ReceivePaymentsPubliclyIntentHandler{ + conf: conf, + data: data, + antispamGuard: antispamGuard, + } +} + +func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error { + return newIntentDeniedError("remote send requires rewrite") + + /* + typedProtoMetadata := protoMetadata.GetReceivePaymentsPublicly() + if typedProtoMetadata == nil { + return errors.New("unexpected metadata proto message") + } + + giftCardVault, err := common.NewAccountFromPublicKeyBytes(typedProtoMetadata.Source.Value) + if err != nil { + return err + } + + usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, exchange_rate_util.GetLatestExchangeRateTime()) + if err != nil { + return errors.Wrap(err, "error getting current usd exchange rate") + } + + // This is an optimization for payment history. Original fiat amounts are not + // easily linked due to the nature of gift cards and the remote send flow. We + // fetch this metadata up front so we don't need to do it every time in history. + giftCardIssuedIntentRecord, err := h.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVault.PublicKey().ToBase58()) + if err == intent.ErrIntentNotFound { + return newIntentValidationError("source is not a remote send gift card") + } else if err != nil { + return err + } + h.cachedGiftCardIssuedIntentRecord = giftCardIssuedIntentRecord + + intentRecord.IntentType = intent.ReceivePaymentsPublicly + intentRecord.ReceivePaymentsPubliclyMetadata = &intent.ReceivePaymentsPubliclyMetadata{ + Source: giftCardVault.PublicKey().ToBase58(), + Quantity: typedProtoMetadata.Quarks, + IsRemoteSend: typedProtoMetadata.IsRemoteSend, + IsReturned: false, + IsIssuerVoidingGiftCard: typedProtoMetadata.IsIssuerVoidingGiftCard, + + OriginalExchangeCurrency: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeCurrency, + OriginalExchangeRate: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.ExchangeRate, + OriginalNativeAmount: giftCardIssuedIntentRecord.SendPrivatePaymentMetadata.NativeAmount, + + UsdMarketValue: usdExchangeRecord.Rate * float64(typedProtoMetadata.Quarks) / float64(common.CoreMintQuarksPerUnit), + } + + if intentRecord.ReceivePaymentsPubliclyMetadata.IsIssuerVoidingGiftCard && intentRecord.InitiatorOwnerAccount != giftCardIssuedIntentRecord.InitiatorOwnerAccount { + return newIntentValidationError("only the issuer can void the gift card") + } + + return nil + */ +} + +func (h *ReceivePaymentsPubliclyIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { + return false, nil +} + +func (h *ReceivePaymentsPubliclyIntentHandler) GetAdditionalAccountsToLock(ctx context.Context, intentRecord *intent.Record) (*lockableAccounts, error) { + if !intentRecord.ReceivePaymentsPubliclyMetadata.IsRemoteSend { + return &lockableAccounts{}, nil + } + + giftCardVaultAccount, err := common.NewAccountFromPublicKeyString(intentRecord.ReceivePaymentsPubliclyMetadata.Source) + if err != nil { + return nil, err + } + + return &lockableAccounts{ + RemoteSendGiftCardVault: giftCardVaultAccount, + }, nil +} + +func (h *ReceivePaymentsPubliclyIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action) error { + typedMetadata := untypedMetadata.GetReceivePaymentsPublicly() + if typedMetadata == nil { + return errors.New("unexpected metadata proto message") + } + + if !typedMetadata.IsRemoteSend { + return newIntentValidationError("only remote send is supported") + } + + if typedMetadata.ExchangeData != nil { + return newIntentValidationError("exchange data cannot be set") + } + + initiatiorOwnerAccount, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) + if err != nil { + return err + } + + giftCardVaultAccount, err := common.NewAccountFromPublicKeyString(intentRecord.ReceivePaymentsPubliclyMetadata.Source) + if err != nil { + return err + } + + initiatorAccountsByType, err := common.GetLatestCodeTimelockAccountRecordsForOwner(ctx, h.data, initiatiorOwnerAccount) + if err != nil { + return err + } + + initiatorAccounts := make([]*common.AccountRecords, 0) + initiatorAccountsByVault := make(map[string]*common.AccountRecords) + for _, batchRecords := range initiatorAccountsByType { + for _, records := range batchRecords { + initiatorAccounts = append(initiatorAccounts, records) + initiatorAccountsByVault[records.General.TokenAccount] = records + } + } + + // + // Part 1: Intent ID validation + // + + err = validateIntentIdIsNotRequest(ctx, h.data, intentRecord.IntentId) + if err != nil { + return err + } + + // + // Part 2: Antispam guard checks against the phone number + // + if !h.conf.disableAntispamChecks.Get(ctx) { + allow, err := h.antispamGuard.AllowReceivePayments(ctx, initiatiorOwnerAccount, true) + if err != nil { + return err + } else if !allow { + return ErrTooManyPayments + } + } + + // + // Part 3: User account validation to determine if it's managed by Code + // + + err = validateAllUserAccountsManagedByCode(ctx, initiatorAccounts) + if err != nil { + return err + } + + // + // Part 4: Gift card account validation + // + + err = validateClaimedGiftCard(ctx, h.data, giftCardVaultAccount, typedMetadata.Quarks) + if err != nil { + return err + } + + // + // Part 5: Local simulation + // + + simResult, err := LocalSimulation(ctx, h.data, actions) + if err != nil { + return err + } + + // + // Part 6: Validate fee payments + // + + err = validateFeePayments(ctx, h.data, intentRecord, simResult) + if err != nil { + return err + } + + // + // Part 7: Validate the individual actions + // + + return h.validateActions( + ctx, + initiatiorOwnerAccount, + initiatorAccountsByType, + initiatorAccountsByVault, + typedMetadata, + actions, + simResult, + ) +} + +func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( + ctx context.Context, + initiatorOwnerAccount *common.Account, + initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, + initiatorAccountsByVault map[string]*common.AccountRecords, + metadata *transactionpb.ReceivePaymentsPubliclyMetadata, + actions []*transactionpb.Action, + simResult *LocalSimulationResult, +) error { + if len(actions) != 1 { + return newIntentValidationError("expected 1 action") + } + + // + // Part 1: Validate source and destination accounts are valid to use + // + + // Note: Already validated to be a claimable gift card elsewhere + source, err := common.NewAccountFromProto(metadata.Source) + if err != nil { + return err + } + + // The destination account must be the latest temporary incoming account + destinationAccountInfo := initiatorAccountsByType[commonpb.AccountType_TEMPORARY_INCOMING][0].General + destinationSimulation, ok := simResult.SimulationsByAccount[destinationAccountInfo.TokenAccount] + if !ok { + return newActionValidationError(actions[0], "must send payment to latest temp incoming account") + } + + // And that temporary incoming account has limited usage + err = validateMinimalTempIncomingAccountUsage(ctx, h.data, destinationAccountInfo) + if err != nil { + return err + } + + // + // Part 2: Validate actions match intent + // + + // + // Part 2.1: Check source account pays exact quark amount to destination in a public withdraw + // + + sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] + if !ok { + return newIntentValidationError("must receive payments from source account") + } else if sourceSimulation.GetDeltaQuarks() != -int64(metadata.Quarks) { + return newActionValidationErrorf(sourceSimulation.Transfers[0].Action, "must receive %d quarks from source account", metadata.Quarks) + } else if sourceSimulation.Transfers[0].IsPrivate || !sourceSimulation.Transfers[0].IsWithdraw { + return newActionValidationError(sourceSimulation.Transfers[0].Action, "transfer must be a public withdraw") + } + + // + // Part 2.2: Check destination account is paid exact quark amount from source account in a public withdraw + // + + if destinationSimulation.GetDeltaQuarks() != int64(metadata.Quarks) { + return newActionValidationErrorf(actions[0], "must receive %d quarks to temp incoming account", metadata.Quarks) + } else if destinationSimulation.Transfers[0].IsPrivate || !destinationSimulation.Transfers[0].IsWithdraw { + return newActionValidationError(sourceSimulation.Transfers[0].Action, "transfer must be a public withdraw") + } + + // + // Part 3: Validate accounts that are opened and closed + // + + if len(simResult.GetOpenedAccounts()) > 0 { + return newIntentValidationError("cannot open any account") + } + + closedAccounts := simResult.GetClosedAccounts() + if len(closedAccounts) != 1 { + return newIntentValidationError("must close 1 account") + } else if closedAccounts[0].TokenAccount.PublicKey().ToBase58() != source.PublicKey().ToBase58() { + return newActionValidationError(actions[0], "must close source account") + } + + // + // Part 4: Generic validation of actions that move money + // + + return validateMoneyMovementActionUserAccounts(intent.ReceivePaymentsPublicly, initiatorAccountsByVault, actions) +} + +func (h *ReceivePaymentsPubliclyIntentHandler) OnSaveToDB(ctx context.Context, intentRecord *intent.Record) error { + return nil +} + +func (h *ReceivePaymentsPubliclyIntentHandler) OnCommittedToDB(ctx context.Context, intentRecord *intent.Record) error { + return nil +} + +func validateAllUserAccountsManagedByCode(ctx context.Context, initiatorAccounts []*common.AccountRecords) error { + // Try to unlock *ANY* latest account, and you're done + for _, accountRecords := range initiatorAccounts { + if !accountRecords.IsManagedByCode(ctx) { + return ErrNotManagedByCode + } + } + + return nil +} + +func validateMoneyMovementActionCount(actions []*transactionpb.Action) error { + var numMoneyMovementActions int + + for _, action := range actions { + switch action.Type.(type) { + case *transactionpb.Action_NoPrivacyWithdraw, + *transactionpb.Action_NoPrivacyTransfer, + *transactionpb.Action_FeePayment: + + numMoneyMovementActions++ + } + } + + // todo: configurable + if numMoneyMovementActions > 50 { + return newIntentDeniedError("too many transfer/exchange/withdraw actions") + } + return nil +} + +// Provides generic and lightweight validation of which accounts owned by a Code +// user can be used in certain actions. This is by no means a comprehensive check. +// Other account types (eg. gift cards, external wallets, etc) and intent-specific +// complex nuances should be handled elsewhere. +func validateMoneyMovementActionUserAccounts( + intentType intent.Type, + initiatorAccountsByVault map[string]*common.AccountRecords, + actions []*transactionpb.Action, +) error { + for _, action := range actions { + var authority, source *common.Account + var err error + + switch typedAction := action.Type.(type) { + case *transactionpb.Action_NoPrivacyTransfer: + // No privacy transfers are always come from a deposit account + + authority, err = common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Authority) + if err != nil { + return err + } + + source, err = common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Source) + if err != nil { + return err + } + + sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] + if !ok || sourceAccountInfo.General.AccountType != commonpb.AccountType_PRIMARY { + return newActionValidationError(action, "source account must be a deposit account") + } + case *transactionpb.Action_NoPrivacyWithdraw: + // No privacy withdraws are used in two ways depending on the intent: + // 1. As the sender of funds from the latest temp outgoing account in a private send + // 2. As a receiver of funds to the latest temp incoming account in a public receive + + authority, err = common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Authority) + if err != nil { + return err + } + + source, err = common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Source) + if err != nil { + return err + } + + destination, err := common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Destination) + if err != nil { + return err + } + + switch intentType { + case intent.ReceivePaymentsPublicly: + destinationAccountInfo, ok := initiatorAccountsByVault[destination.PublicKey().ToBase58()] + if !ok || destinationAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { + return newActionValidationError(action, "source account must be the latest temporary incoming account") + } + } + case *transactionpb.Action_FeePayment: + // Fee payments always come from the latest temporary outgoing account + + authority, err = common.NewAccountFromProto(typedAction.FeePayment.Authority) + if err != nil { + return err + } + + source, err = common.NewAccountFromProto(typedAction.FeePayment.Source) + if err != nil { + return err + } + + sourceAccountInfo, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] + if !ok || sourceAccountInfo.General.AccountType != commonpb.AccountType_TEMPORARY_OUTGOING { + return newActionValidationError(action, "source account must be the latest temporary outgoing account") + } + default: + continue + } + + expectedTimelockVault, err := getExpectedTimelockVaultFromProtoAccount(authority.ToProto()) + if err != nil { + return err + } else if !bytes.Equal(expectedTimelockVault.PublicKey().ToBytes(), source.PublicKey().ToBytes()) { + return newActionValidationErrorf(action, "authority is invalid") + } + } + + return nil +} + +func validateNextTemporaryAccountOpened( + accountType commonpb.AccountType, + initiatorOwnerAccount *common.Account, + initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, + actions []*transactionpb.Action, +) error { + if accountType != commonpb.AccountType_TEMPORARY_INCOMING && accountType != commonpb.AccountType_TEMPORARY_OUTGOING { + return errors.New("unexpected account type") + } + + prevTempAccountRecords, ok := initiatorAccountsByType[accountType] + if !ok { + return errors.New("previous temp account record missing") + } + + // Find the open and close actions + + var openAction *transactionpb.Action + for _, action := range actions { + switch typed := action.Type.(type) { + case *transactionpb.Action_OpenAccount: + if typed.OpenAccount.AccountType == accountType { + if openAction != nil { + return newIntentValidationErrorf("multiple open actions for %s account type", accountType) + } + + openAction = action + } + } + } + + if openAction == nil { + return newIntentValidationErrorf("open account action for %s account type missing", accountType) + } + + if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatorOwnerAccount.PublicKey().ToBytes()) { + return newActionValidationErrorf(openAction, "owner must be %s", initiatorOwnerAccount.PublicKey().ToBase58()) + } + + if bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { + return newActionValidationErrorf(openAction, "authority cannot be %s", initiatorOwnerAccount.PublicKey().ToBase58()) + } + + expectedIndex := prevTempAccountRecords[0].General.Index + 1 + if openAction.GetOpenAccount().Index != expectedIndex { + return newActionValidationErrorf(openAction, "next derivation expected to be %d", expectedIndex) + } + + expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) + if err != nil { + return err + } + + if !bytes.Equal(openAction.GetOpenAccount().Token.Value, expectedVaultAccount.PublicKey().ToBytes()) { + return newActionValidationErrorf(openAction, "token must be %s", expectedVaultAccount.PublicKey().ToBase58()) + } + + return nil +} + +// Assumes only one gift card account is opened per intent +func validateGiftCardAccountOpened( + ctx context.Context, + data code_data.Provider, + initiatorOwnerAccount *common.Account, + initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, + expectedGiftCardVault *common.Account, + actions []*transactionpb.Action, +) error { + var openAction *transactionpb.Action + for _, action := range actions { + switch typed := action.Type.(type) { + case *transactionpb.Action_OpenAccount: + if typed.OpenAccount.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { + if openAction != nil { + return newIntentValidationErrorf("multiple open actions for %s account type", commonpb.AccountType_REMOTE_SEND_GIFT_CARD) + } + + openAction = action + } + } + } + + if openAction == nil { + return newIntentValidationErrorf("open account action for %s account type missing", commonpb.AccountType_REMOTE_SEND_GIFT_CARD) + } + + if bytes.Equal(openAction.GetOpenAccount().Owner.Value, initiatorOwnerAccount.PublicKey().ToBytes()) { + return newActionValidationErrorf(openAction, "owner cannot be %s", initiatorOwnerAccount.PublicKey().ToBase58()) + } + + if !bytes.Equal(openAction.GetOpenAccount().Owner.Value, openAction.GetOpenAccount().Authority.Value) { + return newActionValidationErrorf(openAction, "authority must be %s", openAction.GetOpenAccount().Owner.Value) + } + + if openAction.GetOpenAccount().Index != 0 { + return newActionValidationError(openAction, "index must be 0") + } + + derivedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) + if err != nil { + return err + } + + if !bytes.Equal(expectedGiftCardVault.PublicKey().ToBytes(), derivedVaultAccount.PublicKey().ToBytes()) { + return newActionValidationErrorf(openAction, "token must be %s", expectedGiftCardVault.PublicKey().ToBase58()) + } + + if !bytes.Equal(openAction.GetOpenAccount().Token.Value, derivedVaultAccount.PublicKey().ToBytes()) { + return newActionValidationErrorf(openAction, "token must be %s", derivedVaultAccount.PublicKey().ToBase58()) + } + + if err := validateTimelockUnlockStateDoesntExist(ctx, data, openAction.GetOpenAccount()); err != nil { + return err + } + + return nil +} + +func validateExternalTokenAccountWithinIntent(ctx context.Context, data code_data.Provider, tokenAccount *common.Account) error { + isValid, message, err := common.ValidateExternalTokenAccount(ctx, data, tokenAccount) + if err != nil { + return err + } else if !isValid { + return newIntentValidationError(message) + } + return nil +} + +func validateExchangeDataWithinIntent(ctx context.Context, data code_data.Provider, intentId string, proto *transactionpb.ExchangeData) error { + // If there's a payment request record, then validate exchange data client + // provided matches exactly. The payment request record should already have + // validated exchange data before it was created. + requestRecord, err := data.GetRequest(ctx, intentId) + if err == nil { + if !requestRecord.RequiresPayment() { + return newIntentValidationError("request doesn't require payment") + } + + if proto.Currency != string(*requestRecord.ExchangeCurrency) { + return newIntentValidationErrorf("payment has a request for %s currency", *requestRecord.ExchangeCurrency) + } + + absNativeAmountDiff := math.Abs(proto.NativeAmount - *requestRecord.NativeAmount) + if absNativeAmountDiff > 0.0001 { + return newIntentValidationErrorf("payment has a request for %.2f native amount", *requestRecord.NativeAmount) + } + + // No need to validate exchange details in the payment request. Only Kin has + // exact exchange data requirements, which has already been validated at time + // of payment intent creation. We do leave the ability open to reserve an exchange + // rate, but no use cases warrant that atm. + + } else if err != paymentrequest.ErrPaymentRequestNotFound { + return err + } + + // Otherwise, validate exchange data fully using the common method + isValid, message, err := exchange_rate_util.ValidateClientExchangeData(ctx, data, proto) + if err != nil { + return err + } else if !isValid { + if strings.Contains(message, "stale") { + return newStaleStateError(message) + } + return newIntentValidationError(message) + } + return nil +} + +// Generically validates fee payments as much as possible, but won't cover any +// intent-specific nuances (eg. where the fee payment comes from) +// +// This assumes source and destination accounts interacting with fees and the +// remaining amount don't have minimum bucket size requirements. Intent validation +// logic is responsible for these checks and guarantees. +func validateFeePayments( + ctx context.Context, + data code_data.Provider, + intentRecord *intent.Record, + simResult *LocalSimulationResult, +) error { + var requiresFee bool + var totalQuarksSent uint64 + switch intentRecord.IntentType { + } + + if !requiresFee && simResult.HasAnyFeePayments() { + return newIntentValidationError("intent doesn't require a fee payment") + } + + if requiresFee && !simResult.HasAnyFeePayments() { + return newIntentValidationError("intent requires a fee payment") + } + + if !requiresFee { + return nil + } + + requestRecord, err := data.GetRequest(ctx, intentRecord.IntentId) + if err != nil { + return err + } + + if !requestRecord.RequiresPayment() { + return newIntentValidationError("request doesn't require payment") + } + + additionalRequestedFees := requestRecord.Fees + + feePayments := simResult.GetFeePayments() + if len(feePayments) != len(additionalRequestedFees)+1 { + return newIntentValidationErrorf("expected %d fee payment action", len(additionalRequestedFees)+1) + } + + codeFeePayment := feePayments[0] + + if codeFeePayment.Action.GetFeePayment().Type != transactionpb.FeePaymentAction_CODE { + return newActionValidationError(codeFeePayment.Action, "fee payment type must be CODE") + } + + if codeFeePayment.Action.GetFeePayment().Destination != nil { + return newActionValidationError(codeFeePayment.Action, "code fee payment destination is configured by server") + } + + feeAmount := codeFeePayment.DeltaQuarks + if feeAmount >= 0 { + return newActionValidationError(codeFeePayment.Action, "fee payment amount is negative") + } + feeAmount = -feeAmount // Because it's coming out of a user account in this simulation + + var foundUsdExchangeRecord bool + usdExchangeRecords, err := exchange_rate_util.GetPotentialClientExchangeRates(ctx, data, currency_lib.USD) + if err != nil { + return err + } + for _, exchangeRecord := range usdExchangeRecords { + usdValue := exchangeRecord.Rate * float64(feeAmount) / float64(common.CoreMintQuarksPerUnit) + + // Allow for some small margin of error + // + // todo: Hardcoded as a penny USD, but might want a dynamic amount if we + // have use cases with different fee amounts. + if usdValue > 0.0099 && usdValue < 0.0101 { + foundUsdExchangeRecord = true + break + } + } + + if !foundUsdExchangeRecord { + return newActionValidationError(codeFeePayment.Action, "code fee payment amount must be $0.01 USD") + } + + for i, additionalFee := range feePayments[1:] { + if additionalFee.Action.GetFeePayment().Type != transactionpb.FeePaymentAction_THIRD_PARTY { + return newActionValidationError(additionalFee.Action, "fee payment type must be THIRD_PARTY") + } + + destination := additionalFee.Action.GetFeePayment().Destination + if destination == nil { + return newActionValidationError(additionalFee.Action, "fee payment destination is required") + } + + // The destination should already be validated as a valid payment destination + if base58.Encode(destination.Value) != additionalRequestedFees[i].DestinationTokenAccount { + return newActionValidationErrorf(additionalFee.Action, "fee payment destination must be %s", additionalRequestedFees[i].DestinationTokenAccount) + } + + feeAmount := additionalFee.DeltaQuarks + if feeAmount >= 0 { + return newActionValidationError(additionalFee.Action, "fee payment amount is negative") + } + feeAmount = -feeAmount // Because it's coming out of a user account in this simulation + + requestedAmount := (uint64(additionalRequestedFees[i].BasisPoints) * totalQuarksSent) / 10000 + if feeAmount != int64(requestedAmount) { + return newActionValidationErrorf(additionalFee.Action, "fee payment amount must be for %d bps of total amount", additionalRequestedFees[i].BasisPoints) + } + } + + return nil +} + +func validateMinimalTempIncomingAccountUsage(ctx context.Context, data code_data.Provider, accountInfo *account.Record) error { + if accountInfo.AccountType != commonpb.AccountType_TEMPORARY_INCOMING { + return errors.New("expected a temporary incoming account") + } + + actionRecords, err := data.GetAllActionsByAddress(ctx, accountInfo.TokenAccount) + if err != nil && err != action.ErrActionNotFound { + return err + } + + var paymentCount int + for _, actionRecord := range actionRecords { + // Revoked actions don't count + if actionRecord.State == action.StateRevoked { + continue + } + + // Temp incoming accounts are always paid via no privacy withdraws + if actionRecord.ActionType != action.NoPrivacyWithdraw { + continue + } + + paymentCount += 1 + } + + // Should be coordinated with MustRotate flag in GetTokenAccountInfos + // + // todo: configurable + if paymentCount >= 2 { + // Important Note: Do not leak anything. Just say it isn't the latest. + return newStaleStateError("destination is not the latest temporary incoming account") + } + return nil +} + +func validateClaimedGiftCard(ctx context.Context, data code_data.Provider, giftCardVaultAccount *common.Account, claimedAmount uint64) error { + // + // Part 1: Is the account a gift card? + // + + accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err == account.ErrAccountInfoNotFound || accountInfoRecord.AccountType != commonpb.AccountType_REMOTE_SEND_GIFT_CARD { + return newIntentValidationError("source is not a remote send gift card") + } + + // + // Part 2: Is there already an action to claim the gift card balance? + // + + _, err = data.GetGiftCardClaimedAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err == nil { + return newStaleStateError("gift card balance has already been claimed") + } else if err == action.ErrActionNotFound { + // No action to claim it, so we can proceed + } else if err != nil { + return err + } + + // + // Part 3: Is the action to auto-return the balance back to the issuer in a scheduling or post-scheduling state? + // + + autoReturnActionRecord, err := data.GetGiftCardAutoReturnAction(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err != nil && err != action.ErrActionNotFound { + return err + } + + if err == nil && autoReturnActionRecord.State != action.StateUnknown { + return newStaleStateError("gift card is expired") + } + + // + // Part 4: Is the gift card managed by Code? + // + + timelockRecord, err := data.GetTimelockByVault(ctx, giftCardVaultAccount.PublicKey().ToBase58()) + if err != nil { + return err + } + + isManagedByCode := common.IsManagedByCode(ctx, timelockRecord) + if err != nil { + return err + } else if !isManagedByCode { + if timelockRecord.IsClosed() { + // Better error messaging, since we know we'll never reopen the account + // and the balance is guaranteed to be claimed (not necessarily through + // Code server though). + return newStaleStateError("gift card balance has already been claimed") + } + return ErrNotManagedByCode + } + + // + // Part 5: Is the full amount being claimed? + // + + // We don't track external deposits to gift cards or any further Code transfers + // to it in SubmitIntent, so this check is sufficient for now. + giftCardBalance, err := balance.CalculateFromCache(ctx, data, giftCardVaultAccount) + if err != nil { + return err + } else if giftCardBalance == 0 { + // Shouldn't be hit with checks from part 3, but left for completeness + return newStaleStateError("gift card balance has already been claimed") + } else if giftCardBalance != claimedAmount { + return newIntentValidationErrorf("must receive entire gift card balance of %d quarks", giftCardBalance) + } + + // + // Part 6: Are we within the threshold for auto-return back to the issuer? + // + + // todo: I think we use the same trick of doing deadline - x minutes to avoid race + // conditions without distributed locks. + if time.Since(accountInfoRecord.CreatedAt) > 24*time.Hour-15*time.Minute { + return newStaleStateError("gift card is expired") + } + + return nil +} + +func validateIntentIdIsNotRequest(ctx context.Context, data code_data.Provider, intentId string) error { + _, err := data.GetRequest(ctx, intentId) + if err == nil { + return newIntentDeniedError("intent id is reserved for a request") + } else if err != paymentrequest.ErrPaymentRequestNotFound { + return err + } + return nil +} + +func validateTimelockUnlockStateDoesntExist(ctx context.Context, data code_data.Provider, openAction *transactionpb.OpenAccountAction) error { + authorityAccount, err := common.NewAccountFromProto(openAction.Authority) + if err != nil { + return err + } + + timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + if err != nil { + return err + } + + _, err = data.GetBlockchainAccountInfo(ctx, timelockAccounts.Unlock.PublicKey().ToBase58(), solana.CommitmentFinalized) + switch err { + case nil: + return newIntentDeniedError("an account being opened has already initiated an unlock") + case solana.ErrNoAccountInfo: + return nil + default: + return err + } +} + +func getExpectedTimelockVaultFromProtoAccount(authorityProto *commonpb.SolanaAccountId) (*common.Account, error) { + authorityAccount, err := common.NewAccountFromProto(authorityProto) + if err != nil { + return nil, err + } + + timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + if err != nil { + return nil, err + } + return timelockAccounts.Vault, nil +} diff --git a/pkg/code/server/grpc/transaction/v2/intent_test.go b/pkg/code/server/transaction/intent_test.go similarity index 100% rename from pkg/code/server/grpc/transaction/v2/intent_test.go rename to pkg/code/server/transaction/intent_test.go diff --git a/pkg/code/server/transaction/limits.go b/pkg/code/server/transaction/limits.go new file mode 100644 index 00000000..6bd8b20f --- /dev/null +++ b/pkg/code/server/transaction/limits.go @@ -0,0 +1,66 @@ +package transaction_v2 + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" + + "github.com/code-payments/code-server/pkg/code/common" + "github.com/code-payments/code-server/pkg/code/limit" + "github.com/code-payments/code-server/pkg/grpc/client" +) + +func (s *transactionServer) GetLimits(ctx context.Context, req *transactionpb.GetLimitsRequest) (*transactionpb.GetLimitsResponse, error) { + log := s.log.WithField("method", "GetLimits") + log = client.InjectLoggingMetadata(ctx, log) + + ownerAccount, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner_account", ownerAccount.PublicKey().ToBase58()) + + sig := req.Signature + req.Signature = nil + if err := s.auth.Authenticate(ctx, ownerAccount, req, sig); err != nil { + return nil, err + } + + zeroSendLimits := make(map[string]*transactionpb.SendLimit) + zeroMicroPaymentLimits := make(map[string]*transactionpb.MicroPaymentLimit) + zeroBuyModuleLimits := make(map[string]*transactionpb.BuyModuleLimit) + for currency := range limit.SendLimits { + zeroSendLimits[string(currency)] = &transactionpb.SendLimit{ + NextTransaction: 0, + MaxPerTransaction: 0, + MaxPerDay: 0, + } + zeroBuyModuleLimits[string(currency)] = &transactionpb.BuyModuleLimit{ + MaxPerTransaction: 0, + MinPerTransaction: 0, + } + } + for currency := range limit.MicroPaymentLimits { + zeroMicroPaymentLimits[string(currency)] = &transactionpb.MicroPaymentLimit{ + MaxPerTransaction: 0, + MinPerTransaction: 0, + } + } + zeroResp := &transactionpb.GetLimitsResponse{ + Result: transactionpb.GetLimitsResponse_OK, + SendLimitsByCurrency: zeroSendLimits, + MicroPaymentLimitsByCurrency: zeroMicroPaymentLimits, + BuyModuleLimitsByCurrency: zeroBuyModuleLimits, + DepositLimit: &transactionpb.DepositLimit{ + MaxQuarks: 0, + }, + } + + // todo: We need to calculate limits based on account, or another identity system + // now that phone numbers is no longer a requirements. + return zeroResp, nil +} diff --git a/pkg/code/server/grpc/transaction/v2/limits_test.go b/pkg/code/server/transaction/limits_test.go similarity index 100% rename from pkg/code/server/grpc/transaction/v2/limits_test.go rename to pkg/code/server/transaction/limits_test.go diff --git a/pkg/code/server/grpc/transaction/v2/local_simulation.go b/pkg/code/server/transaction/local_simulation.go similarity index 86% rename from pkg/code/server/grpc/transaction/v2/local_simulation.go rename to pkg/code/server/transaction/local_simulation.go index 1e4f5485..7cf2220e 100644 --- a/pkg/code/server/grpc/transaction/v2/local_simulation.go +++ b/pkg/code/server/transaction/local_simulation.go @@ -222,106 +222,12 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr }, }, ) - case *transactionpb.Action_TemporaryPrivacyTransfer: - source, err := common.NewAccountFromProto(typedAction.TemporaryPrivacyTransfer.Source) - if err != nil { - return nil, err - } - derivedTimelockVault = source - - authority, err = common.NewAccountFromProto(typedAction.TemporaryPrivacyTransfer.Authority) - if err != nil { - return nil, err - } - - destination, err := common.NewAccountFromProto(typedAction.TemporaryPrivacyTransfer.Destination) - if err != nil { - return nil, err - } - - amount := typedAction.TemporaryPrivacyTransfer.Amount - - simulations = append( - simulations, - TokenAccountSimulation{ - TokenAccount: source, - Transfers: []TransferSimulation{ - { - Action: action, - IsPrivate: true, - IsWithdraw: false, - IsFee: false, - DeltaQuarks: -int64(amount), - }, - }, - }, - TokenAccountSimulation{ - TokenAccount: destination, - Transfers: []TransferSimulation{ - { - Action: action, - IsPrivate: true, - IsWithdraw: false, - IsFee: false, - DeltaQuarks: int64(amount), - }, - }, - }, - ) - case *transactionpb.Action_TemporaryPrivacyExchange: - source, err := common.NewAccountFromProto(typedAction.TemporaryPrivacyExchange.Source) - if err != nil { - return nil, err - } - derivedTimelockVault = source - - authority, err = common.NewAccountFromProto(typedAction.TemporaryPrivacyExchange.Authority) - if err != nil { - return nil, err - } - - destination, err := common.NewAccountFromProto(typedAction.TemporaryPrivacyExchange.Destination) - if err != nil { - return nil, err - } - - amount := typedAction.TemporaryPrivacyExchange.Amount - - simulations = append( - simulations, - TokenAccountSimulation{ - TokenAccount: source, - Transfers: []TransferSimulation{ - { - Action: action, - IsPrivate: true, - IsWithdraw: false, - IsFee: false, - DeltaQuarks: -int64(amount), - }, - }, - }, - TokenAccountSimulation{ - TokenAccount: destination, - Transfers: []TransferSimulation{ - { - Action: action, - IsPrivate: true, - IsWithdraw: false, - IsFee: false, - DeltaQuarks: int64(amount), - }, - }, - }, - ) - case *transactionpb.Action_PermanentPrivacyUpgrade: - continue default: return nil, errors.New("unhandled action for local simulation") } // Validate authorities and respective derived timelock vault accounts match. - timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.KinMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) if err != nil { return nil, err } diff --git a/pkg/code/server/grpc/transaction/v2/local_simulation_test.go b/pkg/code/server/transaction/local_simulation_test.go similarity index 100% rename from pkg/code/server/grpc/transaction/v2/local_simulation_test.go rename to pkg/code/server/transaction/local_simulation_test.go diff --git a/pkg/code/server/grpc/transaction/v2/metrics.go b/pkg/code/server/transaction/metrics.go similarity index 100% rename from pkg/code/server/grpc/transaction/v2/metrics.go rename to pkg/code/server/transaction/metrics.go diff --git a/pkg/code/server/grpc/transaction/v2/onramp.go b/pkg/code/server/transaction/onramp.go similarity index 82% rename from pkg/code/server/grpc/transaction/v2/onramp.go rename to pkg/code/server/transaction/onramp.go index f0c0fee9..79a7513b 100644 --- a/pkg/code/server/grpc/transaction/v2/onramp.go +++ b/pkg/code/server/transaction/onramp.go @@ -13,7 +13,6 @@ import ( "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/onramp" - "github.com/code-payments/code-server/pkg/code/data/phone" "github.com/code-payments/code-server/pkg/code/limit" currency_lib "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/grpc/client" @@ -44,19 +43,6 @@ func (s *transactionServer) DeclareFiatOnrampPurchaseAttempt(ctx context.Context return nil, err } - verificationRecord, err := s.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58()) - switch err { - case nil: - case phone.ErrVerificationNotFound: - return &transactionpb.DeclareFiatOnrampPurchaseAttemptResponse{ - Result: transactionpb.DeclareFiatOnrampPurchaseAttemptResponse_INVALID_OWNER, - }, nil - default: - log.WithError(err).Warn("failure getting phone verification record") - return nil, status.Error(codes.Internal, "") - } - log = log.WithField("phone_number", verificationRecord.PhoneNumber) - currency := currency_lib.Code(strings.ToLower(req.PurchaseAmount.Currency)) amount := req.PurchaseAmount.NativeAmount @@ -69,7 +55,7 @@ func (s *transactionServer) DeclareFiatOnrampPurchaseAttempt(ctx context.Context // Validate the purchase amount makes sense within defined limits sendLimit, ok := limit.SendLimits[currency] - if !ok || currency == currency_lib.KIN { + if !ok { return &transactionpb.DeclareFiatOnrampPurchaseAttemptResponse{ Result: transactionpb.DeclareFiatOnrampPurchaseAttemptResponse_UNSUPPORTED_CURRENCY, }, nil diff --git a/pkg/code/server/grpc/transaction/v2/onramp_test.go b/pkg/code/server/transaction/onramp_test.go similarity index 100% rename from pkg/code/server/grpc/transaction/v2/onramp_test.go rename to pkg/code/server/transaction/onramp_test.go diff --git a/pkg/code/server/grpc/transaction/v2/server.go b/pkg/code/server/transaction/server.go similarity index 60% rename from pkg/code/server/grpc/transaction/v2/server.go rename to pkg/code/server/transaction/server.go index 3b852327..becbda08 100644 --- a/pkg/code/server/grpc/transaction/v2/server.go +++ b/pkg/code/server/transaction/server.go @@ -4,7 +4,6 @@ import ( "context" "sync" - "github.com/oschwald/maxminddb-golang" "github.com/sirupsen/logrus" transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" @@ -14,10 +13,8 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/lawenforcement" - "github.com/code-payments/code-server/pkg/code/server/grpc/messaging" + "github.com/code-payments/code-server/pkg/code/server/messaging" "github.com/code-payments/code-server/pkg/jupiter" - "github.com/code-payments/code-server/pkg/kin" - push_lib "github.com/code-payments/code-server/pkg/push" sync_util "github.com/code-payments/code-server/pkg/sync" ) @@ -29,28 +26,17 @@ type transactionServer struct { auth *auth_util.RPCSignatureVerifier - pusher push_lib.Provider - jupiterClient *jupiter.Client messagingClient messaging.InternalMessageClient - maxmind *maxminddb.Reader antispamGuard *antispam.Guard amlGuard *lawenforcement.AntiMoneyLaunderingGuard - // todo: A better way of managing this if/when we do a treasury per individual transaction amount - treasuryPoolNameByBaseAmount map[uint64]string - - treasuryPoolStatsLock sync.RWMutex - treasuryPoolStatsInitialLoadWgByName map[string]*sync.WaitGroup - currentTreasuryPoolStatsByName map[string]*treasuryPoolStats - // todo: distributed locks intentLocks *sync_util.StripedLock ownerLocks *sync_util.StripedLock giftCardLocks *sync_util.StripedLock - phoneLocks *sync_util.StripedLock airdropperLock sync.Mutex airdropper *common.TimelockAccounts @@ -64,10 +50,8 @@ type transactionServer struct { func NewTransactionServer( data code_data.Provider, - pusher push_lib.Provider, jupiterClient *jupiter.Client, messagingClient messaging.InternalMessageClient, - maxmind *maxminddb.Reader, antispamGuard *antispam.Guard, configProvider ConfigProvider, ) transactionpb.TransactionServer { @@ -85,43 +69,16 @@ func NewTransactionServer( auth: auth_util.NewRPCSignatureVerifier(data), - pusher: pusher, - jupiterClient: jupiterClient, messagingClient: messagingClient, - maxmind: maxmind, antispamGuard: antispamGuard, amlGuard: lawenforcement.NewAntiMoneyLaunderingGuard(data), intentLocks: sync_util.NewStripedLock(stripedLockParallelization), ownerLocks: sync_util.NewStripedLock(stripedLockParallelization), giftCardLocks: sync_util.NewStripedLock(stripedLockParallelization), - phoneLocks: sync_util.NewStripedLock(stripedLockParallelization), - } - - s.treasuryPoolNameByBaseAmount = map[uint64]string{ - kin.ToQuarks(1): s.conf.treasuryPoolOneKinBucket.Get(ctx), - kin.ToQuarks(10): s.conf.treasuryPoolTenKinBucket.Get(ctx), - kin.ToQuarks(100): s.conf.treasuryPoolHundredKinBucket.Get(ctx), - kin.ToQuarks(1_000): s.conf.treasuryPoolThousandKinBucket.Get(ctx), - kin.ToQuarks(10_000): s.conf.treasuryPoolTenThousandKinBucket.Get(ctx), - kin.ToQuarks(100_000): s.conf.treasuryPoolHundredThousandKinBucket.Get(ctx), - kin.ToQuarks(1_000_000): s.conf.treasuryPoolMillionKinBucket.Get(ctx), - } - s.currentTreasuryPoolStatsByName = make(map[string]*treasuryPoolStats) - s.treasuryPoolStatsInitialLoadWgByName = make(map[string]*sync.WaitGroup) - - for _, treasury := range s.treasuryPoolNameByBaseAmount { - var wg sync.WaitGroup - wg.Add(1) - - s.treasuryPoolStatsLock.Lock() - s.treasuryPoolStatsInitialLoadWgByName[treasury] = &wg - s.treasuryPoolStatsLock.Unlock() - - go s.treasuryPoolMonitor(ctx, treasury) } airdropper := s.conf.airdropperOwnerPublicKey.Get(ctx) diff --git a/pkg/code/server/grpc/transaction/v2/swap.go b/pkg/code/server/transaction/swap.go similarity index 88% rename from pkg/code/server/grpc/transaction/v2/swap.go rename to pkg/code/server/transaction/swap.go index ee4214af..4ff382de 100644 --- a/pkg/code/server/grpc/transaction/v2/swap.go +++ b/pkg/code/server/transaction/swap.go @@ -12,24 +12,16 @@ import ( "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" "github.com/code-payments/code-server/pkg/code/balance" - chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/localization" - push_util "github.com/code-payments/code-server/pkg/code/push" currency_lib "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/grpc/client" "github.com/code-payments/code-server/pkg/jupiter" - "github.com/code-payments/code-server/pkg/kin" "github.com/code-payments/code-server/pkg/solana" compute_budget "github.com/code-payments/code-server/pkg/solana/computebudget" "github.com/code-payments/code-server/pkg/solana/cvm" @@ -51,6 +43,11 @@ func (s *transactionServer) Swap(streamer transactionpb.Transaction_SwapServer) return handleSwapError(streamer, status.Error(codes.Unavailable, "")) } + if common.CoreMintAccount.PublicKey().ToBase58() == common.UsdcMintAccount.PublicKey().ToBase58() { + log.Warn("core mint account is usdc") + return handleSwapError(streamer, status.Error(codes.Unavailable, "")) + } + req, err := s.boundedSwapRecv(ctx, streamer) if err != nil { log.WithError(err).Info("error receiving request from client") @@ -150,7 +147,7 @@ func (s *transactionServer) Swap(streamer transactionpb.Transaction_SwapServer) } // todo: This might come from a DB record at some point - vmDepositTokenAddress, err := token.GetAssociatedAccount(vmDepositPda, kin.TokenMint) + vmDepositTokenAddress, err := token.GetAssociatedAccount(vmDepositPda, common.CoreMintAccount.PublicKey().ToBytes()) if err != nil { log.WithError(err).Warn("failure deriving vm deposit token account") return handleSwapError(streamer, err) @@ -189,7 +186,7 @@ func (s *transactionServer) Swap(streamer transactionpb.Transaction_SwapServer) quote, err := s.jupiterClient.GetQuote( ctx, usdc.Mint, - kin.Mint, + common.CoreMintAccount.PublicKey().ToBase58(), amountToSwap, 50, // todo: configurable slippage or something based on liquidity? true, // Direct routes for now since we're using legacy instructions @@ -391,8 +388,6 @@ func (s *transactionServer) Swap(streamer transactionpb.Transaction_SwapServer) // Section: Transaction submission // - // chatMessageTs := time.Now() - _, err = s.data.SubmitBlockchainTransaction(ctx, &txn) if err != nil { log.WithError(err).Warn("failure submitting transaction") @@ -405,11 +400,6 @@ func (s *transactionServer) Swap(streamer transactionpb.Transaction_SwapServer) log.WithField("txn", base64.StdEncoding.EncodeToString(txn.Marshal())).Info("transaction submitted") - // err = s.bestEffortNotifyUserOfSwapInProgress(ctx, owner, chatMessageTs) - // if err != nil { - // log.WithError(err).Warn("failure notifying user of swap in progress") - // } - if !initiateReq.WaitForBlockchainStatus { err = streamer.Send(&transactionpb.SwapResponse{ Response: &transactionpb.SwapResponse_Success_{ @@ -506,8 +496,8 @@ func (s *transactionServer) validateSwap( } usdcAmount := float64(amountToSwap) / float64(usdc.QuarksPerUsdc) - kinAmount := float64(quote.GetEstimatedSwapAmount()) / float64(kin.QuarksPerKin) - swapRate := usdcAmount / kinAmount + otherAmount := float64(quote.GetEstimatedSwapAmount()) / float64(common.CoreMintQuarksPerUnit) + swapRate := usdcAmount / otherAmount usdExchangeRateRateRecord, err := s.data.GetExchangeRate(ctx, currency_lib.USD, time.Now()) if err != nil { @@ -523,56 +513,6 @@ func (s *transactionServer) validateSwap( return nil } -func (s *transactionServer) bestEffortNotifyUserOfSwapInProgress(ctx context.Context, owner *common.Account, ts time.Time) error { - chatId := chat_util.GetKinPurchasesChatId(owner) - - // Inspect the chat history for a USDC deposited message. If that message - // doesn't exist, then avoid sending the swap in progress chat message, since - // it can lead to user confusion. - chatMessageRecords, err := s.data.GetAllChatMessages(ctx, chatId, query.WithDirection(query.Descending), query.WithLimit(1)) - switch err { - case nil: - var protoChatMessage chatpb.ChatMessage - err := proto.Unmarshal(chatMessageRecords[0].Data, &protoChatMessage) - if err != nil { - return errors.Wrap(err, "error unmarshalling proto chat message") - } - - switch typed := protoChatMessage.Content[0].Type.(type) { - case *chatpb.Content_ServerLocalized: - if typed.ServerLocalized.KeyOrText != localization.ChatMessageUsdcDeposited { - return nil - } - } - case chat.ErrMessageNotFound: - default: - return errors.Wrap(err, "error fetching chat messages") - } - - chatMessage, err := chat_util.NewUsdcBeingConvertedMessage(ts) - if err != nil { - return errors.Wrap(err, "error creating chat message") - } - - canPush, err := chat_util.SendKinPurchasesMessage(ctx, s.data, owner, chatMessage) - if err != nil { - return errors.Wrap(err, "error sending chat message") - } - - if canPush { - push_util.SendChatMessagePushNotification( - ctx, - s.data, - s.pusher, - chat_util.KinPurchasesName, - owner, - chatMessage, - ) - } - - return nil -} - func (s *transactionServer) mustLoadSwapSubsidizer(ctx context.Context) { log := s.log.WithFields(logrus.Fields{ "method": "mustLoadSwapSubsidizer", diff --git a/pkg/code/server/grpc/transaction/v2/testutil.go b/pkg/code/server/transaction/testutil.go similarity index 100% rename from pkg/code/server/grpc/transaction/v2/testutil.go rename to pkg/code/server/transaction/testutil.go diff --git a/pkg/code/server/web/request/client.go b/pkg/code/server/web/request/client.go deleted file mode 100644 index e1503259..00000000 --- a/pkg/code/server/web/request/client.go +++ /dev/null @@ -1,130 +0,0 @@ -package request - -import ( - "context" - - "github.com/mr-tron/base58" - "github.com/pkg/errors" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" - micropaymentpb "github.com/code-payments/code-protobuf-api/generated/go/micropayment/v1" - userpb "github.com/code-payments/code-protobuf-api/generated/go/user/v1" - - "github.com/code-payments/code-server/pkg/code/common" -) - -// todo: Migrate to a generic HTTP -> gRPC with signed proto strategy - -func (s *Server) createTrustlessRequest(ctx context.Context, request *trustlessRequest) (err error) { - return s.createRequest( - ctx, - &messagingpb.SendMessageRequest{ - RendezvousKey: &messagingpb.RendezvousKey{ - Value: request.GetPublicRendezvousKey().PublicKey().ToBytes(), - }, - Message: request.ToProtoMessage(), - Signature: &commonpb.Signature{ - Value: request.clientSignature[:], - }, - }, - request.webhookUrl, - ) -} - -func (s *Server) createRequest( - ctx context.Context, - signedCreateRequest *messagingpb.SendMessageRequest, - webhookUrl *string, -) error { - messagingClient := messagingpb.NewMessagingClient(s.cc) - microPaymentClient := micropaymentpb.NewMicroPaymentClient(s.cc) - - // - // Part 1: Create the request. - // - - // Note that retries are acceptable - createResp, err := messagingClient.SendMessage(ctx, signedCreateRequest) - if err != nil { - return err - } else if createResp.Result != messagingpb.SendMessageResponse_OK { - return errors.Errorf("send message result %s", createResp.Result) - } - - // - // Part 2: Register webhook if URL was provided - // - - if webhookUrl != nil { - registerWebhookReq := µpaymentpb.RegisterWebhookRequest{ - IntentId: &commonpb.IntentId{ - Value: signedCreateRequest.RendezvousKey.Value, - }, - Url: *webhookUrl, - } - - registerWebhookResp, err := microPaymentClient.RegisterWebhook(ctx, registerWebhookReq) - if err != nil { - return err - } - - switch registerWebhookResp.Result { - case micropaymentpb.RegisterWebhookResponse_OK, micropaymentpb.RegisterWebhookResponse_ALREADY_REGISTERED: - default: - return errors.Errorf("register webhook result %s", createResp.Result) - } - } - - return nil -} - -type intentStatusResp struct { - Status string -} - -func (s *Server) getIntentStatus(ctx context.Context, intentId *common.Account) (*intentStatusResp, error) { - microPaymentClient := micropaymentpb.NewMicroPaymentClient(s.cc) - - getStatusReq := µpaymentpb.GetStatusRequest{ - IntentId: &commonpb.IntentId{ - Value: intentId.PublicKey().ToBytes(), - }, - } - - getStatusResp, err := microPaymentClient.GetStatus(ctx, getStatusReq) - if err != nil { - return nil, err - } - - res := intentStatusResp{ - Status: "UNKNOWN", - } - - if getStatusResp.IntentSubmitted { - res.Status = "SUBMITTED" - } else if getStatusResp.Exists { - res.Status = "PENDING" - } - - return &res, nil -} - -type getUserIdResp struct { - User string -} - -func (s *Server) getUserId(ctx context.Context, protoReq *userpb.GetLoginForThirdPartyAppRequest) (*getUserIdResp, error) { - userIdentityClient := userpb.NewIdentityClient(s.cc) - - getLoginResp, err := userIdentityClient.GetLoginForThirdPartyApp(ctx, protoReq) - if err != nil { - return nil, err - } - - res := &getUserIdResp{} - if getLoginResp.Result == userpb.GetLoginForThirdPartyAppResponse_OK { - res.User = base58.Encode(getLoginResp.UserId.Value) - } - return res, nil -} diff --git a/pkg/code/server/web/request/model.go b/pkg/code/server/web/request/model.go deleted file mode 100644 index 12ec083c..00000000 --- a/pkg/code/server/web/request/model.go +++ /dev/null @@ -1,163 +0,0 @@ -package request - -import ( - "encoding/base64" - "encoding/json" - "io" - "net/http" - - "github.com/mr-tron/base58" - "github.com/pkg/errors" - "google.golang.org/protobuf/proto" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" - userpb "github.com/code-payments/code-protobuf-api/generated/go/user/v1" - - "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/netutil" - "github.com/code-payments/code-server/pkg/solana" -) - -// todo: Migrate to a generic HTTP -> gRPC with signed proto strategy - -type trustlessRequest struct { - originalProtoMessage *messagingpb.Message - publicRendezvousKey *common.Account - clientSignature solana.Signature // For a messagingpb.RequestToReceiveBill - webhookUrl *string -} - -func newTrustlessRequest( - originalProtoMessage *messagingpb.Message, - publicRendezvousKey *common.Account, - clientSignature solana.Signature, - webhookUrl *string, -) (*trustlessRequest, error) { - return &trustlessRequest{ - originalProtoMessage: originalProtoMessage, - publicRendezvousKey: publicRendezvousKey, - clientSignature: clientSignature, - webhookUrl: webhookUrl, - }, nil -} - -func newTrustlessRequestFromHttpContext(r *http.Request) (*trustlessRequest, error) { - httpRequestBody := struct { - Intent string `json:"intent"` - Message string `json:"message"` - Signature string `json:"signature"` - Webhook *string `json:"webhook"` - }{} - - body, err := io.ReadAll(r.Body) - if err != nil { - return nil, err - } - - err = json.Unmarshal(body, &httpRequestBody) - if err != nil { - return nil, err - } - - rendezvousKey, err := common.NewAccountFromPublicKeyString(httpRequestBody.Intent) - if err != nil { - return nil, errors.New("intent is not a public key") - } - - var signature solana.Signature - decodedSignature, err := base58.Decode(httpRequestBody.Signature) - if err != nil || len(decodedSignature) != len(signature) { - return nil, errors.New("signature is invalid") - } - copy(signature[:], decodedSignature) - - var requestToReceiveBill messagingpb.RequestToReceiveBill - messageBytes, err := base64.RawURLEncoding.DecodeString(httpRequestBody.Message) - if err != nil { - return nil, errors.New("message not valid base64") - } - err = proto.Unmarshal(messageBytes, &requestToReceiveBill) - if err != nil { - return nil, errors.New("message bytes is not a RequestToReceiveBill") - } - - // Note: Validation occurs at the messaging service - protoMessage := &messagingpb.Message{ - Kind: &messagingpb.Message_RequestToReceiveBill{ - RequestToReceiveBill: &requestToReceiveBill, - }, - } - - if httpRequestBody.Webhook != nil { - err = netutil.ValidateHttpUrl(*httpRequestBody.Webhook, true, false) - if err != nil { - return nil, err - } - } - - return newTrustlessRequest( - protoMessage, - rendezvousKey, - signature, - httpRequestBody.Webhook, - ) -} - -func (r *trustlessRequest) GetPublicRendezvousKey() *common.Account { - return r.publicRendezvousKey -} - -func (r *trustlessRequest) GetClientSignature() solana.Signature { - return r.clientSignature -} - -func (r *trustlessRequest) ToProtoMessage() *messagingpb.Message { - return r.originalProtoMessage -} - -func newGetLoggedInUserIdRequestFromHttpContext(r *http.Request) (*userpb.GetLoginForThirdPartyAppRequest, error) { - httpRequestBody := struct { - Intent string `json:"intent"` - Message string `json:"message"` - Signature string `json:"signature"` - }{} - - body, err := io.ReadAll(r.Body) - if err != nil { - return nil, err - } - - err = json.Unmarshal(body, &httpRequestBody) - if err != nil { - return nil, err - } - - intentId, err := common.NewAccountFromPublicKeyString(httpRequestBody.Intent) - if err != nil { - return nil, errors.New("intent is not a public key") - } - - var protoRequest userpb.GetLoginForThirdPartyAppRequest - decoded, err := base64.RawURLEncoding.DecodeString(httpRequestBody.Message) - if err != nil { - return nil, errors.New("message not valid base64") - } - err = proto.Unmarshal(decoded, &protoRequest) - if err != nil { - return nil, errors.New("message bytes is not a GetLoginForThirdPartyAppRequest") - } else if err := protoRequest.Validate(); err != nil { - return nil, errors.Wrap(err, "message failed proto validation") - } - - var signature solana.Signature - decodedSignature, err := base58.Decode(httpRequestBody.Signature) - if err != nil || len(decodedSignature) != len(signature) { - return nil, errors.New("signature is invalid") - } - copy(signature[:], decodedSignature) - - protoRequest.IntentId = &commonpb.IntentId{Value: intentId.ToProto().Value} - protoRequest.Signature = &commonpb.Signature{Value: decodedSignature} - return &protoRequest, nil -} diff --git a/pkg/code/server/web/request/server.go b/pkg/code/server/web/request/server.go deleted file mode 100644 index 95730471..00000000 --- a/pkg/code/server/web/request/server.go +++ /dev/null @@ -1,150 +0,0 @@ -package request - -import ( - "errors" - "net/http" - - "github.com/sirupsen/logrus" - "google.golang.org/grpc" - - "github.com/code-payments/code-server/pkg/code/common" -) - -const ( - v1PathPrefix = "/v1" - v1CreateIntentPath = v1PathPrefix + "/createIntent" - v1GetStatusPath = v1PathPrefix + "/getStatus" - v1GetUserIdPath = v1PathPrefix + "/getUserId" - - contentTypeHeaderName = "content-type" - jsonContentTypeHeaderValue = "application/json" -) - -type Server struct { - log *logrus.Entry - cc *grpc.ClientConn -} - -func NewRequestServer(cc *grpc.ClientConn) *Server { - return &Server{ - log: logrus.StandardLogger().WithField("type", "request/server"), - cc: cc, - } -} - -func (s *Server) createIntentHandler(path string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - log := s.log.WithField("path", path) - - statusCode, body := func() (int, GenericApiResponseBody) { - ctx := r.Context() - - if r.Method != http.MethodPost { - return http.StatusBadRequest, NewGenericApiFailureResponseBody(errors.New("http post expected")) - } - - model, err := newTrustlessRequestFromHttpContext(r) - if err != nil { - return http.StatusBadRequest, NewGenericApiFailureResponseBody(err) - } - - err = s.createTrustlessRequest(ctx, model) - if err != nil { - log.WithError(err).Warn("failure creating request") - statusCode, err := HandleGrpcErrorInWebContext(w, err) - return statusCode, NewGenericApiFailureResponseBody(err) - } - - return http.StatusOK, NewGenericApiSuccessResponseBody() - }() - - w.Header().Set(contentTypeHeaderName, jsonContentTypeHeaderValue) - w.WriteHeader(statusCode) - w.Write([]byte(body.ToString())) - } -} - -// todo: migrate to a POST variant -func (s *Server) getStatusHandler(path string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - log := s.log.WithField("path", path) - - statusCode, body := func() (int, GenericApiResponseBody) { - ctx := r.Context() - - if r.Method != http.MethodGet { - return http.StatusBadRequest, NewGenericApiFailureResponseBody(errors.New("http get expected")) - } - - intentIdQueryParam := r.URL.Query()["intent"] - if len(intentIdQueryParam) < 1 { - return http.StatusBadRequest, NewGenericApiFailureResponseBody(errors.New("intent query parameter missing")) - } - - intentId, err := common.NewAccountFromPublicKeyString(intentIdQueryParam[0]) - if err != nil { - return http.StatusBadRequest, NewGenericApiFailureResponseBody(errors.New("intent id is not a public key")) - } - log = log.WithField("intent", intentId.PublicKey().ToBase58()) - - res, err := s.getIntentStatus(ctx, intentId) - if err != nil { - log.WithError(err).Warn("failure getting intent status") - statusCode, err := HandleGrpcErrorInWebContext(w, err) - return statusCode, NewGenericApiFailureResponseBody(err) - } - - respBody := NewGenericApiSuccessResponseBody() - respBody["status"] = res.Status - return http.StatusOK, respBody - }() - - w.Header().Set(contentTypeHeaderName, jsonContentTypeHeaderValue) - w.WriteHeader(statusCode) - w.Write([]byte(body.ToString())) - } -} - -func (s *Server) getUserIdHandler(path string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - log := s.log.WithField("path", path) - - statusCode, body := func() (int, GenericApiResponseBody) { - ctx := r.Context() - - if r.Method != http.MethodPost { - return http.StatusBadRequest, NewGenericApiFailureResponseBody(errors.New("http post expected")) - } - - protoReq, err := newGetLoggedInUserIdRequestFromHttpContext(r) - if err != nil { - return http.StatusBadRequest, NewGenericApiFailureResponseBody(err) - } - - res, err := s.getUserId(ctx, protoReq) - if err != nil { - log.WithError(err).Warn("failure getting user") - statusCode, err := HandleGrpcErrorInWebContext(w, err) - return statusCode, NewGenericApiFailureResponseBody(err) - } - - respBody := NewGenericApiSuccessResponseBody() - if len(res.User) > 0 { - respBody["user"] = res.User - } - return http.StatusOK, respBody - }() - - w.Header().Set(contentTypeHeaderName, jsonContentTypeHeaderValue) - w.WriteHeader(statusCode) - w.Write([]byte(body.ToString())) - } -} - -func (s *Server) GetHandlers() map[string]http.HandlerFunc { - return map[string]http.HandlerFunc{ - v1CreateIntentPath: s.createIntentHandler(v1CreateIntentPath), - v1GetStatusPath: s.getStatusHandler(v1GetStatusPath), - v1GetUserIdPath: s.getUserIdHandler(v1GetUserIdPath), - } -} diff --git a/pkg/code/server/web/request/util.go b/pkg/code/server/web/request/util.go deleted file mode 100644 index 04bdb1d1..00000000 --- a/pkg/code/server/web/request/util.go +++ /dev/null @@ -1,63 +0,0 @@ -package request - -// todo: put all of this somewhere more common - -import ( - "encoding/json" - "errors" - "net/http" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -const ( - successJsonKey = "success" - errorJsonKey = "error" -) - -type GenericApiResponseBody map[string]any - -func NewGenericApiSuccessResponseBody() GenericApiResponseBody { - return map[string]any{ - successJsonKey: true, - } -} - -func NewGenericApiFailureResponseBody(err error) GenericApiResponseBody { - return map[string]any{ - successJsonKey: false, - errorJsonKey: err.Error(), - } -} - -func (b *GenericApiResponseBody) ToString() string { - marshalled, _ := json.Marshal(b) - return string(marshalled) -} - -func HandleGrpcErrorInWebContext(w http.ResponseWriter, err error) (int, error) { - if err == nil { - return http.StatusOK, nil - } - - statusErr, ok := status.FromError(err) - if !ok { - return http.StatusInternalServerError, errors.New("internal server error") - } - - switch statusErr.Code() { - case codes.OK: - return http.StatusOK, nil - case codes.InvalidArgument: - return http.StatusBadRequest, err - case codes.Unauthenticated: - return http.StatusUnauthorized, errors.New("authentication failed") - case codes.PermissionDenied: - return http.StatusForbidden, errors.New("permission denied") - case codes.Canceled, codes.DeadlineExceeded: - return http.StatusRequestTimeout, errors.New("request timed out") - default: - return http.StatusInternalServerError, errors.New("internal server error") - } -} diff --git a/pkg/code/thirdparty/message.go b/pkg/code/thirdparty/message.go index 03ed2aee..cf7319a0 100644 --- a/pkg/code/thirdparty/message.go +++ b/pkg/code/thirdparty/message.go @@ -5,7 +5,6 @@ import ( "crypto/rand" "encoding/binary" "strings" - "time" "github.com/google/uuid" "github.com/jdgcs/ed25519/extra25519" @@ -13,9 +12,6 @@ import ( "github.com/pkg/errors" "github.com/vence722/base122-go" "golang.org/x/crypto/nacl/box" - "google.golang.org/protobuf/types/known/timestamppb" - - chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/netutil" @@ -113,41 +109,6 @@ func (m *NaclBoxBlockchainMessage) Encode() ([]byte, error) { return base122.Encode(buffer) } -// ToProto creates the proto representation of a NaclBoxBlockchainMessage -func (m *NaclBoxBlockchainMessage) ToProto( - sender *common.Account, - signature string, - ts time.Time, -) (*chatpb.ChatMessage, error) { - messageId, err := base58.Decode(signature) - if err != nil { - return nil, err - } - - msg := &chatpb.ChatMessage{ - MessageId: &chatpb.ChatMessageId{ - Value: messageId, - }, - Ts: timestamppb.New(ts), - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_NaclBox{ - NaclBox: &chatpb.NaclBoxEncryptedContent{ - PeerPublicKey: sender.ToProto(), - Nonce: m.Nonce, - EncryptedPayload: m.EncryptedMessage, - }, - }, - }, - }, - } - - if err := msg.Content[0].Validate(); err != nil { - return nil, errors.Wrap(err, "unexpectedly constructed an invalid proto message") - } - return msg, nil -} - // DecodeNaclBoxBlockchainMessage attempts to decode a byte payload into a NaclBoxBlockchainMessage func DecodeNaclBoxBlockchainMessage(payload []byte) (*NaclBoxBlockchainMessage, error) { errInvalidPayload := errors.New("invalid payload") diff --git a/pkg/code/transaction/instruction.go b/pkg/code/transaction/instruction.go index 41cee0cd..868b7d29 100644 --- a/pkg/code/transaction/instruction.go +++ b/pkg/code/transaction/instruction.go @@ -1,10 +1,9 @@ package transaction import ( + "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/solana" - splitter_token "github.com/code-payments/code-server/pkg/solana/splitter" "github.com/code-payments/code-server/pkg/solana/system" - "github.com/code-payments/code-server/pkg/code/common" ) // todo: start moving instruction construction code here @@ -15,31 +14,3 @@ func makeAdvanceNonceInstruction(nonce *common.Account) (solana.Instruction, err common.GetSubsidizer().PublicKey().ToBytes(), ), nil } - -func makeTransferWithCommitmentInstruction( - treasuryPool *common.Account, - treasuryPoolVault *common.Account, - destination *common.Account, - commitment *common.Account, - treasuryPoolBump uint8, - kinAmountInQuarks uint64, - transcript []byte, - recentRoot []byte, -) (solana.Instruction, error) { - return splitter_token.NewTransferWithCommitmentInstruction( - &splitter_token.TransferWithCommitmentInstructionAccounts{ - Pool: treasuryPool.PublicKey().ToBytes(), - Vault: treasuryPoolVault.PublicKey().ToBytes(), - Destination: destination.PublicKey().ToBytes(), - Commitment: commitment.PublicKey().ToBytes(), - Authority: common.GetSubsidizer().PublicKey().ToBytes(), - Payer: common.GetSubsidizer().PublicKey().ToBytes(), - }, - &splitter_token.TransferWithCommitmentInstructionArgs{ - PoolBump: treasuryPoolBump, - Amount: kinAmountInQuarks, - Transcript: transcript, - RecentRoot: recentRoot, - }, - ).ToLegacyInstruction(), nil -} diff --git a/pkg/code/transaction/memo.go b/pkg/code/transaction/memo.go deleted file mode 100644 index 4dfd49c8..00000000 --- a/pkg/code/transaction/memo.go +++ /dev/null @@ -1,41 +0,0 @@ -package transaction - -import ( - "encoding/base64" - "errors" - "fmt" - - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/solana" - "github.com/code-payments/code-server/pkg/solana/memo" - - transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" -) - -const ( - KreAppIndex = 268 -) - -// MakeKreMemoInstruction makes the KRE memo instruction required for KRE payouts -// -// todo: Deprecate this with KRE gone -func MakeKreMemoInstruction() (solana.Instruction, error) { - kreMemo, err := kin.NewMemo(kin.HighestVersion, kin.TransactionTypeP2P, KreAppIndex, nil) - if err != nil { - return solana.Instruction{}, err - } - return memo.Instruction(base64.StdEncoding.EncodeToString(kreMemo[:])), nil -} - -// GetTipMemoValue gets the string value provided in a memo for tips -func GetTipMemoValue(platform transactionpb.TippedUser_Platform, username string) (string, error) { - var platformName string - switch platform { - case transactionpb.TippedUser_TWITTER: - platformName = "X" - default: - return "", errors.New("unexpeted tip platform") - } - - return fmt.Sprintf("tip:%s:%s", platformName, username), nil -} diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index a60dca03..49b12a04 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -274,166 +274,6 @@ func MakeExternalTransferWithAuthorityTransaction( return MakeNoncedTransaction(nonce, bh, execInstruction) } -func MakeInternalTreasuryAdvanceTransaction( - nonce *common.Account, - bh solana.Blockhash, - - vm *common.Account, - accountMemory *common.Account, - accountIndex uint16, - relayMemory *common.Account, - relayIndex uint16, - - treasuryPool *common.Account, - treasuryPoolVault *common.Account, - commitment *common.Account, - kinAmountInQuarks uint64, - transcript []byte, - recentRoot []byte, -) (solana.Transaction, error) { - treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) - treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) - - mergedMemoryBanks, err := MergeMemoryBanks(accountMemory, relayMemory) - if err != nil { - return solana.Transaction{}, err - } - - vixn := cvm.NewRelayVirtualInstruction(&cvm.RelayVirtualInstructionArgs{ - Amount: kinAmountInQuarks, - Transcript: cvm.Hash(transcript), - RecentRoot: cvm.Hash(recentRoot), - Commitment: cvm.Hash(commitment.PublicKey().ToBytes()), - }) - - execInstruction := cvm.NewExecInstruction( - &cvm.ExecInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), - VmMemA: mergedMemoryBanks.A, - VmMemB: mergedMemoryBanks.B, - VmRelay: &treasuryPoolPublicKeyBytes, - VmRelayVault: &treasuryPoolVaultPublicKeyBytes, - }, - &cvm.ExecInstructionArgs{ - Opcode: vixn.Opcode, - MemIndices: []uint16{accountIndex, relayIndex}, - MemBanks: mergedMemoryBanks.Indices, - Data: vixn.Data, - }, - ) - - return MakeNoncedTransaction(nonce, bh, execInstruction) -} - -func MakeExternalTreasuryAdvanceTransaction( - nonce *common.Account, - bh solana.Blockhash, - - vm *common.Account, - relayMemory *common.Account, - relayIndex uint16, - - treasuryPool *common.Account, - treasuryPoolVault *common.Account, - destination *common.Account, - commitment *common.Account, - kinAmountInQuarks uint64, - transcript []byte, - recentRoot []byte, -) (solana.Transaction, error) { - memoryAPublicKeyBytes := ed25519.PublicKey(relayMemory.PublicKey().ToBytes()) - - treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) - treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) - - externalAddressPublicKeyBytes := ed25519.PublicKey(destination.PublicKey().ToBytes()) - - vixn := cvm.NewExternalRelayVirtualInstruction(&cvm.ExternalRelayVirtualInstructionArgs{ - Amount: kinAmountInQuarks, - Transcript: cvm.Hash(transcript), - RecentRoot: cvm.Hash(recentRoot), - Commitment: cvm.Hash(commitment.PublicKey().ToBytes()), - }) - - execInstruction := cvm.NewExecInstruction( - &cvm.ExecInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), - VmMemA: &memoryAPublicKeyBytes, - VmRelay: &treasuryPoolPublicKeyBytes, - VmRelayVault: &treasuryPoolVaultPublicKeyBytes, - ExternalAddress: &externalAddressPublicKeyBytes, - }, - &cvm.ExecInstructionArgs{ - Opcode: vixn.Opcode, - MemIndices: []uint16{relayIndex}, - MemBanks: []uint8{0}, - Data: vixn.Data, - }, - ) - - return MakeNoncedTransaction(nonce, bh, execInstruction) -} - -func MakeCashChequeTransaction( - nonce *common.Account, - bh solana.Blockhash, - - virtualSignature solana.Signature, - - vm *common.Account, - vmOmnibus *common.Account, - - nonceMemory *common.Account, - nonceIndex uint16, - sourceMemory *common.Account, - sourceIndex uint16, - relayMemory *common.Account, - relayIndex uint16, - - treasuryPool *common.Account, - treasuryPoolVault *common.Account, - kinAmountInQuarks uint64, -) (solana.Transaction, error) { - vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) - - treasuryPoolPublicKeyBytes := ed25519.PublicKey(treasuryPool.PublicKey().ToBytes()) - treasuryPoolVaultPublicKeyBytes := ed25519.PublicKey(treasuryPoolVault.PublicKey().ToBytes()) - - mergedMemoryBanks, err := MergeMemoryBanks(nonceMemory, sourceMemory, relayMemory) - if err != nil { - return solana.Transaction{}, err - } - - vixn := cvm.NewConditionalTransferVirtualInstruction(&cvm.ConditionalTransferVirtualInstructionArgs{ - Amount: kinAmountInQuarks, - Signature: cvm.Signature(virtualSignature), - }) - - execInstruction := cvm.NewExecInstruction( - &cvm.ExecInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), - VmMemA: mergedMemoryBanks.A, - VmMemB: mergedMemoryBanks.B, - VmMemC: mergedMemoryBanks.C, - VmOmnibus: &vmOmnibusPublicKeyBytes, - VmRelay: &treasuryPoolPublicKeyBytes, - VmRelayVault: &treasuryPoolVaultPublicKeyBytes, - ExternalAddress: &treasuryPoolVaultPublicKeyBytes, - }, - &cvm.ExecInstructionArgs{ - Opcode: vixn.Opcode, - MemIndices: []uint16{nonceIndex, sourceIndex, relayIndex}, - MemBanks: mergedMemoryBanks.Indices, - Data: vixn.Data, - }, - ) - - return MakeNoncedTransaction(nonce, bh, execInstruction) -} - type MergedMemoryBankResult struct { A *ed25519.PublicKey B *ed25519.PublicKey diff --git a/pkg/code/webhook/execution.go b/pkg/code/webhook/execution.go index 7573e7f6..9aee9ca3 100644 --- a/pkg/code/webhook/execution.go +++ b/pkg/code/webhook/execution.go @@ -13,11 +13,11 @@ import ( messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" - "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/webhook" - "github.com/code-payments/code-server/pkg/code/server/grpc/messaging" + "github.com/code-payments/code-server/pkg/code/server/messaging" + "github.com/code-payments/code-server/pkg/metrics" ) const ( diff --git a/pkg/code/webhook/execution_test.go b/pkg/code/webhook/execution_test.go index b95c88a8..6a60281f 100644 --- a/pkg/code/webhook/execution_test.go +++ b/pkg/code/webhook/execution_test.go @@ -1,32 +1,6 @@ package webhook -import ( - "context" - "crypto/ed25519" - "strings" - "testing" - "time" - - "github.com/golang-jwt/jwt/v5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" - messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" - - "github.com/code-payments/code-server/pkg/code/common" - code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/intent" - "github.com/code-payments/code-server/pkg/code/data/paymentrequest" - "github.com/code-payments/code-server/pkg/code/data/webhook" - "github.com/code-payments/code-server/pkg/code/server/grpc/messaging" - "github.com/code-payments/code-server/pkg/kin" - "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/testutil" -) +/* func TestWebhook_HappyPath_IntentSubmmitted(t *testing.T) { env := setup(t) @@ -154,7 +128,7 @@ func (e *testEnv) setupIntentRecord(t *testing.T, webhookRecord *webhook.Record) ExchangeCurrency: "usd", NativeAmount: 12.3, ExchangeRate: 0.1, - Quantity: kin.ToQuarks(123), + Quantity: common.ToCoreMintQuarks(123), DestinationTokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), IsMicroPayment: true, UsdMarketValue: 12.3, @@ -199,7 +173,7 @@ func (e *testEnv) setupRelationshipAccount(t *testing.T, owner string) *account. OwnerAccount: owner, AuthorityAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), TokenAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), - MintAccount: common.KinMintAccount.PublicKey().ToBase58(), + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), AccountType: commonpb.AccountType_RELATIONSHIP, Index: 0, @@ -226,3 +200,4 @@ func (e *testEnv) assertNoWebhookCalledMessagesSent(t *testing.T, webhookRecord require.NoError(t, err) assert.Empty(t, messageRecords) } +*/ diff --git a/pkg/code/webhook/payload.go b/pkg/code/webhook/payload.go index 9c65905b..fd1f05c4 100644 --- a/pkg/code/webhook/payload.go +++ b/pkg/code/webhook/payload.go @@ -48,13 +48,6 @@ func intentSubmittedJsonPayloadProvider(ctx context.Context, data code_data.Prov var destination string var isMicroPayment bool switch intentRecord.IntentType { - case intent.SendPrivatePayment: - currency = intentRecord.SendPrivatePaymentMetadata.ExchangeCurrency - amount = intentRecord.SendPrivatePaymentMetadata.NativeAmount - exchangeRate = intentRecord.SendPrivatePaymentMetadata.ExchangeRate - quarks = intentRecord.SendPrivatePaymentMetadata.Quantity - destination = intentRecord.SendPrivatePaymentMetadata.DestinationTokenAccount - isMicroPayment = intentRecord.SendPrivatePaymentMetadata.IsMicroPayment default: return nil, errors.Errorf("%d intent type is not supported", intentRecord.IntentType) } diff --git a/pkg/currency/iso.go b/pkg/currency/iso.go index d7b869a1..0c906705 100644 --- a/pkg/currency/iso.go +++ b/pkg/currency/iso.go @@ -3,176 +3,176 @@ package currency type Code string const ( - KIN Code = "kin" - AED Code = "aed" - AFN Code = "afn" - ALL Code = "all" - AMD Code = "amd" - ANG Code = "ang" - AOA Code = "aoa" - ARS Code = "ars" - AUD Code = "aud" - AWG Code = "awg" - AZN Code = "azn" - BAM Code = "bam" - BBD Code = "bbd" - BDT Code = "bdt" - BGN Code = "bgn" - BHD Code = "bhd" - BIF Code = "bif" - BMD Code = "bmd" - BND Code = "bnd" - BOB Code = "bob" - BRL Code = "brl" - BSD Code = "bsd" - BTC Code = "btc" - BTN Code = "btn" - BWP Code = "bwp" - BYN Code = "byn" - BYR Code = "byr" - BZD Code = "bzd" - CAD Code = "cad" - CDF Code = "cdf" - CHF Code = "chf" - CLF Code = "clf" - CLP Code = "clp" - CNY Code = "cny" - COP Code = "cop" - CRC Code = "crc" - CUC Code = "cuc" - CUP Code = "cup" - CVE Code = "cve" - CZK Code = "czk" - DJF Code = "djf" - DKK Code = "dkk" - DOP Code = "dop" - DZD Code = "dzd" - EGP Code = "egp" - ERN Code = "ern" - ETB Code = "etb" - EUR Code = "eur" - FJD Code = "fjd" - FKP Code = "fkp" - GBP Code = "gbp" - GEL Code = "gel" - GGP Code = "ggp" - GHS Code = "ghs" - GIP Code = "gip" - GMD Code = "gmd" - GNF Code = "gnf" - GTQ Code = "gtq" - GYD Code = "gyd" - HKD Code = "hkd" - HNL Code = "hnl" - HRK Code = "hrk" - HTG Code = "htg" - HUF Code = "huf" - IDR Code = "idr" - ILS Code = "ils" - IMP Code = "imp" - INR Code = "inr" - IQD Code = "iqd" - IRR Code = "irr" - ISK Code = "isk" - JEP Code = "jep" - JMD Code = "jmd" - JOD Code = "jod" - JPY Code = "jpy" - KES Code = "kes" - KGS Code = "kgs" - KHR Code = "khr" - KMF Code = "kmf" - KPW Code = "kpw" - KRW Code = "krw" - KWD Code = "kwd" - KYD Code = "kyd" - KZT Code = "kzt" - LAK Code = "lak" - LBP Code = "lbp" - LKR Code = "lkr" - LRD Code = "lrd" - LSL Code = "lsl" - LTL Code = "ltl" - LVL Code = "lvl" - LYD Code = "lyd" - MAD Code = "mad" - MDL Code = "mdl" - MGA Code = "mga" - MKD Code = "mkd" - MMK Code = "mmk" - MNT Code = "mnt" - MOP Code = "mop" - MRO Code = "mro" - MRU Code = "mru" - MUR Code = "mur" - MVR Code = "mvr" - MWK Code = "mwk" - MXN Code = "mxn" - MYR Code = "myr" - MZN Code = "mzn" - NAD Code = "nad" - NGN Code = "ngn" - NIO Code = "nio" - NOK Code = "nok" - NPR Code = "npr" - NZD Code = "nzd" - OMR Code = "omr" - PAB Code = "pab" - PEN Code = "pen" - PGK Code = "pgk" - PHP Code = "php" - PKR Code = "pkr" - PLN Code = "pln" - PYG Code = "pyg" - QAR Code = "qar" - RON Code = "ron" - RSD Code = "rsd" - RUB Code = "rub" - RWF Code = "rwf" - SAR Code = "sar" - SBD Code = "sbd" - SCR Code = "scr" - SDG Code = "sdg" - SEK Code = "sek" - SGD Code = "sgd" - SHP Code = "shp" - SLL Code = "sll" - SOS Code = "sos" - SRD Code = "srd" - SSP Code = "ssp" - STD Code = "std" - STN Code = "stn" - SVC Code = "svc" - SYP Code = "syp" - SZL Code = "szl" - THB Code = "thb" - TJS Code = "tjs" - TMT Code = "tmt" - TND Code = "tnd" - TOP Code = "top" - TRY Code = "try" - TTD Code = "ttd" - TWD Code = "twd" - TZS Code = "tzs" - UAH Code = "uah" - UGX Code = "ugx" - USD Code = "usd" - UYU Code = "uyu" - UZS Code = "uzs" - VES Code = "ves" - VND Code = "vnd" - VUV Code = "vuv" - WST Code = "wst" - XAF Code = "xaf" - XAG Code = "xag" - XAU Code = "xau" - XCD Code = "xcd" - XDR Code = "xdr" - XOF Code = "xof" - XPF Code = "xpf" - YER Code = "yer" - ZAR Code = "zar" - ZMK Code = "zmk" - ZMW Code = "zmw" - ZWL Code = "zwl" + USDC Code = "usdc" + AED Code = "aed" + AFN Code = "afn" + ALL Code = "all" + AMD Code = "amd" + ANG Code = "ang" + AOA Code = "aoa" + ARS Code = "ars" + AUD Code = "aud" + AWG Code = "awg" + AZN Code = "azn" + BAM Code = "bam" + BBD Code = "bbd" + BDT Code = "bdt" + BGN Code = "bgn" + BHD Code = "bhd" + BIF Code = "bif" + BMD Code = "bmd" + BND Code = "bnd" + BOB Code = "bob" + BRL Code = "brl" + BSD Code = "bsd" + BTC Code = "btc" + BTN Code = "btn" + BWP Code = "bwp" + BYN Code = "byn" + BYR Code = "byr" + BZD Code = "bzd" + CAD Code = "cad" + CDF Code = "cdf" + CHF Code = "chf" + CLF Code = "clf" + CLP Code = "clp" + CNY Code = "cny" + COP Code = "cop" + CRC Code = "crc" + CUC Code = "cuc" + CUP Code = "cup" + CVE Code = "cve" + CZK Code = "czk" + DJF Code = "djf" + DKK Code = "dkk" + DOP Code = "dop" + DZD Code = "dzd" + EGP Code = "egp" + ERN Code = "ern" + ETB Code = "etb" + EUR Code = "eur" + FJD Code = "fjd" + FKP Code = "fkp" + GBP Code = "gbp" + GEL Code = "gel" + GGP Code = "ggp" + GHS Code = "ghs" + GIP Code = "gip" + GMD Code = "gmd" + GNF Code = "gnf" + GTQ Code = "gtq" + GYD Code = "gyd" + HKD Code = "hkd" + HNL Code = "hnl" + HRK Code = "hrk" + HTG Code = "htg" + HUF Code = "huf" + IDR Code = "idr" + ILS Code = "ils" + IMP Code = "imp" + INR Code = "inr" + IQD Code = "iqd" + IRR Code = "irr" + ISK Code = "isk" + JEP Code = "jep" + JMD Code = "jmd" + JOD Code = "jod" + JPY Code = "jpy" + KES Code = "kes" + KGS Code = "kgs" + KHR Code = "khr" + KMF Code = "kmf" + KPW Code = "kpw" + KRW Code = "krw" + KWD Code = "kwd" + KYD Code = "kyd" + KZT Code = "kzt" + LAK Code = "lak" + LBP Code = "lbp" + LKR Code = "lkr" + LRD Code = "lrd" + LSL Code = "lsl" + LTL Code = "ltl" + LVL Code = "lvl" + LYD Code = "lyd" + MAD Code = "mad" + MDL Code = "mdl" + MGA Code = "mga" + MKD Code = "mkd" + MMK Code = "mmk" + MNT Code = "mnt" + MOP Code = "mop" + MRO Code = "mro" + MRU Code = "mru" + MUR Code = "mur" + MVR Code = "mvr" + MWK Code = "mwk" + MXN Code = "mxn" + MYR Code = "myr" + MZN Code = "mzn" + NAD Code = "nad" + NGN Code = "ngn" + NIO Code = "nio" + NOK Code = "nok" + NPR Code = "npr" + NZD Code = "nzd" + OMR Code = "omr" + PAB Code = "pab" + PEN Code = "pen" + PGK Code = "pgk" + PHP Code = "php" + PKR Code = "pkr" + PLN Code = "pln" + PYG Code = "pyg" + QAR Code = "qar" + RON Code = "ron" + RSD Code = "rsd" + RUB Code = "rub" + RWF Code = "rwf" + SAR Code = "sar" + SBD Code = "sbd" + SCR Code = "scr" + SDG Code = "sdg" + SEK Code = "sek" + SGD Code = "sgd" + SHP Code = "shp" + SLL Code = "sll" + SOS Code = "sos" + SRD Code = "srd" + SSP Code = "ssp" + STD Code = "std" + STN Code = "stn" + SVC Code = "svc" + SYP Code = "syp" + SZL Code = "szl" + THB Code = "thb" + TJS Code = "tjs" + TMT Code = "tmt" + TND Code = "tnd" + TOP Code = "top" + TRY Code = "try" + TTD Code = "ttd" + TWD Code = "twd" + TZS Code = "tzs" + UAH Code = "uah" + UGX Code = "ugx" + USD Code = "usd" + UYU Code = "uyu" + UZS Code = "uzs" + VES Code = "ves" + VND Code = "vnd" + VUV Code = "vuv" + WST Code = "wst" + XAF Code = "xaf" + XAG Code = "xag" + XAU Code = "xau" + XCD Code = "xcd" + XDR Code = "xdr" + XOF Code = "xof" + XPF Code = "xpf" + YER Code = "yer" + ZAR Code = "zar" + ZMK Code = "zmk" + ZMW Code = "zmw" + ZWL Code = "zwl" ) diff --git a/pkg/device/android/verifier.go b/pkg/device/android/verifier.go deleted file mode 100644 index 7e4a3484..00000000 --- a/pkg/device/android/verifier.go +++ /dev/null @@ -1,143 +0,0 @@ -package android - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/pkg/errors" - "google.golang.org/api/playintegrity/v1" - - "github.com/code-payments/code-server/pkg/device" - "github.com/code-payments/code-server/pkg/metrics" -) - -const ( - metricsStructName = "device.android.verifier" -) - -type androidDeviceVerifier struct { - playIntegrity *playintegrity.Service - packageName string - minVersion int64 -} - -// NewAndroidDeviceVerifier returns a new device.Verifier for Android devices -// -// Requires GOOGLE_APPLICATION_CREDENTIALS to be set -func NewAndroidDeviceVerifier(packageName string, minVersion int64) (device.Verifier, error) { - ctx := context.Background() - - playIntegrityService, err := playintegrity.NewService(ctx) - if err != nil { - return nil, err - } - - return &androidDeviceVerifier{ - playIntegrity: playIntegrityService, - packageName: packageName, - minVersion: minVersion, - }, nil -} - -// IsValid implements device.Verifier.IsValid -func (v *androidDeviceVerifier) IsValid(ctx context.Context, token string) (bool, string, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "IsValid") - defer tracer.End() - - isValid, reason, err := func() (bool, string, error) { - resp, err := v.playIntegrity.V1.DecodeIntegrityToken(v.packageName, &playintegrity.DecodeIntegrityTokenRequest{ - IntegrityToken: token, - }).Context(ctx).Do() - if err != nil { - return false, "", err - } - - if resp.ServerResponse.HTTPStatusCode != http.StatusOK { - return false, "", errors.Errorf("received http status %d", resp.ServerResponse.HTTPStatusCode) - } - - requestedAt := time.UnixMilli(resp.TokenPayloadExternal.RequestDetails.TimestampMillis) - if time.Since(requestedAt) > time.Minute { - return false, "device token is too old", nil - } - - if resp.TokenPayloadExternal.AccountDetails == nil { - return false, "account details missing", nil - } - if resp.TokenPayloadExternal.AppIntegrity == nil { - return false, "app integrity missing", nil - } - if resp.TokenPayloadExternal.DeviceIntegrity == nil { - return false, "device integrity missing", nil - } - - if resp.TokenPayloadExternal.AppIntegrity.AppRecognitionVerdict != "PLAY_RECOGNIZED" { - return false, fmt.Sprintf("app recognition verdict is %s", resp.TokenPayloadExternal.AppIntegrity.AppRecognitionVerdict), nil - } - - if resp.TokenPayloadExternal.AccountDetails.AppLicensingVerdict != "LICENSED" { - return false, fmt.Sprintf("app licensing verdict is %s", resp.TokenPayloadExternal.AccountDetails.AppLicensingVerdict), nil - } - - if len(resp.TokenPayloadExternal.DeviceIntegrity.DeviceRecognitionVerdict) == 0 { - return false, "no device recognition verdicts", nil - } - for _, deviceRecognitionVerdict := range resp.TokenPayloadExternal.DeviceIntegrity.DeviceRecognitionVerdict { - switch deviceRecognitionVerdict { - case "MEETS_VIRTUAL_INTEGRITY", "UNKNOWN": - return false, fmt.Sprintf("device recognition verdict is %s", deviceRecognitionVerdict), nil - } - } - - if resp.TokenPayloadExternal.AppIntegrity.PackageName != v.packageName { - return false, fmt.Sprintf("package name is %s", resp.TokenPayloadExternal.AppIntegrity.PackageName), nil - } - - if resp.TokenPayloadExternal.AppIntegrity.VersionCode < v.minVersion { - return false, "minimum client version not met", nil - } - - return true, "", nil - }() - - if err != nil { - tracer.OnError(err) - } - return isValid, reason, err -} - -// HasCreatedFreeAccount implements device.Verifier.HasCreatedFreeAccount -// -// todo: Currently impossible to implement without a stable device ID -func (v *androidDeviceVerifier) HasCreatedFreeAccount(ctx context.Context, token string) (bool, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "HasCreatedFreeAccount") - defer tracer.End() - - hasFreeCreatedAccount, err := func() (bool, error) { - return false, nil - }() - - if err != nil { - tracer.OnError(err) - } - return hasFreeCreatedAccount, err -} - -// MarkCreatedFreeAccount implements device.Verifier.MarkCreatedFreeAccount -// -// todo: Currently impossible to implement without a stable device ID -func (v *androidDeviceVerifier) MarkCreatedFreeAccount(ctx context.Context, token string) error { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "MarkCreatedFreeAccount") - defer tracer.End() - - err := func() error { - return nil - }() - - if err != nil { - tracer.OnError(err) - } - return err -} diff --git a/pkg/device/composite/verifier.go b/pkg/device/composite/verifier.go deleted file mode 100644 index b6847808..00000000 --- a/pkg/device/composite/verifier.go +++ /dev/null @@ -1,86 +0,0 @@ -package composite - -import ( - "context" - - "github.com/code-payments/code-server/pkg/cache" - "github.com/code-payments/code-server/pkg/device" - "github.com/code-payments/code-server/pkg/grpc/client" -) - -type compositeDeviceVerifier struct { - // todo: Something with persistence like Redis to account for multi-server and deploys - // todo: This may not be needed if there's a concept of a production token that has a more aggressive expiry - usedTokenCache cache.Cache - - verifiers map[client.DeviceType]device.Verifier -} - -// NewCompositeDeviceVerifier returns a new device.Verifier that intelligently -// selects device.Verifier instance to verify a token using the client's user -// agent. It also provides a layer of protection against replay attacks by -// enforcing a single use per token value. -// -// If an unknown device type is detected, or no device.Verifier is registered, -// then the device is deemed invalid. -func NewCompositeDeviceVerifier(verifiers map[client.DeviceType]device.Verifier) device.Verifier { - if verifiers == nil { - verifiers = make(map[client.DeviceType]device.Verifier) - } - return &compositeDeviceVerifier{ - usedTokenCache: cache.NewCache(100_000), - verifiers: verifiers, - } -} - -// IsValid implements device.Verifier.IsValid -func (v *compositeDeviceVerifier) IsValid(ctx context.Context, token string) (bool, string, error) { - // Enforce one-time use tokens, even when the third-party verifier doesn't - // do a good job of doing so. - if _, ok := v.usedTokenCache.Retrieve(token); ok { - return false, "device token already consumed", nil - } - - verifier, ok := v.getDeviceVerifier(ctx) - if !ok { - return false, "no device verifier for user agent", nil - } - - isValid, reason, err := verifier.IsValid(ctx, token) - if isValid { - v.usedTokenCache.Insert(token, true, 1) - } - return isValid, reason, err -} - -// HasCreatedFreeAccount implements device.Verifier.HasCreatedFreeAccount -func (v *compositeDeviceVerifier) HasCreatedFreeAccount(ctx context.Context, token string) (bool, error) { - verifier, ok := v.getDeviceVerifier(ctx) - if !ok { - return false, nil - } - return verifier.HasCreatedFreeAccount(ctx, token) -} - -// MarkCreatedFreeAccount implements device.Verifier.MarkCreatedFreeAccount -func (v *compositeDeviceVerifier) MarkCreatedFreeAccount(ctx context.Context, token string) error { - verifier, ok := v.getDeviceVerifier(ctx) - if !ok { - return nil - } - return verifier.MarkCreatedFreeAccount(ctx, token) -} - -func (v *compositeDeviceVerifier) getDeviceVerifier(ctx context.Context) (device.Verifier, bool) { - userAgent, err := client.GetUserAgent(ctx) - if err != nil { - return nil, false - } - - if userAgent.DeviceType == client.DeviceTypeUnknown { - return nil, false - } - - verifier, ok := v.verifiers[userAgent.DeviceType] - return verifier, ok -} diff --git a/pkg/device/ios/verifier.go b/pkg/device/ios/verifier.go deleted file mode 100644 index 14d74c78..00000000 --- a/pkg/device/ios/verifier.go +++ /dev/null @@ -1,142 +0,0 @@ -package ios - -import ( - "context" - "errors" - "strings" - - devicecheck "github.com/rinchsan/device-check-go" - - "github.com/code-payments/code-server/pkg/device" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/metrics" -) - -const ( - metricsStructName = "device.ios.verifier" -) - -type AppleEnv uint8 - -const ( - AppleEnvDevelopment AppleEnv = iota - AppleEnvProduction -) - -// Current two-bit configuration: -// - bit0: Free account flag -// - bit1: Unused -// -// todo: May need a small refactor if bit1 is used -type iOSDeviceVerifier struct { - client *devicecheck.Client - minVersion *client.Version -} - -// NewIOSDeviceVerifier returns a new device.Verifier for iOS devices -func NewIOSDeviceVerifier( - env AppleEnv, - keyIssuer string, - keyId string, - privateKeyFile string, - minVersion *client.Version, -) (device.Verifier, error) { - var dcEnv devicecheck.Environment - switch env { - case AppleEnvDevelopment: - dcEnv = devicecheck.Development - case AppleEnvProduction: - dcEnv = devicecheck.Production - default: - return nil, errors.New("invalid environment") - } - - client := devicecheck.New( - devicecheck.NewCredentialFile(privateKeyFile), - devicecheck.NewConfig(keyIssuer, keyId, dcEnv), - ) - return &iOSDeviceVerifier{ - client: client, - minVersion: minVersion, - }, nil -} - -// IsValid implements device.Verifier.IsValid -func (v *iOSDeviceVerifier) IsValid(ctx context.Context, token string) (bool, string, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "IsValid") - defer tracer.End() - - isValid, reason, err := func() (bool, string, error) { - userAgent, err := client.GetUserAgent(ctx) - if err != nil { - return false, "user agent not set", nil - } - - if userAgent.DeviceType != client.DeviceTypeIOS { - return false, "user agent is not ios", nil - } - - if userAgent.Version.Before(v.minVersion) { - return false, "minimum client version not met", nil - } - - err = v.client.ValidateDeviceToken(token) - if err == nil { - return true, "", nil - } - - // Need to parse for the "bad device token" type of errors. Otherwise, we - // cannot distinguish between a validity issue or other API/network error. - // - // https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_per-device_data#2910408 - errorString := strings.ToLower(err.Error()) - if strings.Contains(errorString, "bad device token") { - return false, "invalid device token", nil - } else if strings.Contains(errorString, "missing or incorrectly formatted device token payload") { - return false, "invalid device token", nil - } - return false, "", err - }() - - if err != nil { - tracer.OnError(err) - } - return isValid, reason, err -} - -// HasCreatedFreeAccount implements device.Verifier.HasCreatedFreeAccount -func (v *iOSDeviceVerifier) HasCreatedFreeAccount(ctx context.Context, token string) (bool, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "HasCreatedFreeAccount") - defer tracer.End() - - hasCreatedFreeAccount, err := func() (bool, error) { - var res devicecheck.QueryTwoBitsResult - err := v.client.QueryTwoBits(token, &res) - if err == nil { - return res.Bit0, nil - } - - errorString := strings.ToLower(err.Error()) - if strings.Contains(errorString, "bit state not found") { - return false, nil - } - return false, err - }() - - if err != nil { - tracer.OnError(err) - } - return hasCreatedFreeAccount, err -} - -// MarkCreatedFreeAccount implements device.Verifier.MarkCreatedFreeAccount -func (v *iOSDeviceVerifier) MarkCreatedFreeAccount(ctx context.Context, token string) error { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "MarkCreatedFreeAccount") - defer tracer.End() - - err := v.client.UpdateTwoBits(token, true, false) - if err != nil { - tracer.OnError(err) - } - return err -} diff --git a/pkg/device/memory/verifier.go b/pkg/device/memory/verifier.go deleted file mode 100644 index 37e9d94b..00000000 --- a/pkg/device/memory/verifier.go +++ /dev/null @@ -1,52 +0,0 @@ -package memory - -import ( - "context" - "errors" - - "github.com/code-payments/code-server/pkg/device" -) - -const ( - ValidDeviceToken = "valid-device-token" - InvalidDeviceToken = "invalid-device-token" -) - -type memoryDeviceVerifier struct { - createdFreeAccount bool -} - -// NewMemoryDeviceVerifier returns a new device.Verifier for testing -func NewMemoryDeviceVerifier() device.Verifier { - return &memoryDeviceVerifier{} -} - -// IsValid implements device.Verifier.IsValid -func (v *memoryDeviceVerifier) IsValid(_ context.Context, token string) (bool, string, error) { - var reason string - if token != ValidDeviceToken { - reason = "invalid test device token" - } - - return token == ValidDeviceToken, reason, nil -} - -// HasCreatedFreeAccount implements device.Verifier.HasCreatedFreeAccount -func (v *memoryDeviceVerifier) HasCreatedFreeAccount(ctx context.Context, token string) (bool, error) { - if token != ValidDeviceToken { - return false, errors.New("invalid device token") - } - - return v.createdFreeAccount, nil -} - -// MarkCreatedFreeAccount implements device.Verifier.MarkCreatedFreeAccount -func (v *memoryDeviceVerifier) MarkCreatedFreeAccount(ctx context.Context, token string) error { - if token != ValidDeviceToken { - return errors.New("invalid device token") - } - - v.createdFreeAccount = true - - return nil -} diff --git a/pkg/device/verifier.go b/pkg/device/verifier.go deleted file mode 100644 index fe3813a1..00000000 --- a/pkg/device/verifier.go +++ /dev/null @@ -1,14 +0,0 @@ -package device - -import "context" - -type Verifier interface { - // IsValid determines a device's validity using an opaque device token. - IsValid(ctx context.Context, token string) (bool, string, error) - - // HasCreatedFreeAccount verifies whether the device has created a free account - HasCreatedFreeAccount(ctx context.Context, token string) (bool, error) - - // MarkCreatedFreeAccount marks the device as having created a free account - MarkCreatedFreeAccount(ctx context.Context, token string) error -} diff --git a/pkg/grpc/metrics/new_relic_server_interceptor.go b/pkg/grpc/metrics/new_relic_server_interceptor.go index 78182b96..d4b802bb 100644 --- a/pkg/grpc/metrics/new_relic_server_interceptor.go +++ b/pkg/grpc/metrics/new_relic_server_interceptor.go @@ -43,54 +43,34 @@ const ( var ( statusCodeHandlers = map[codes.Code]statusCodeHandler{ - codes.OK: infoStatusCodeHandler, - codes.AlreadyExists: infoStatusCodeHandler, - codes.Canceled: infoStatusCodeHandler, - codes.InvalidArgument: infoStatusCodeHandler, - codes.NotFound: infoStatusCodeHandler, - codes.Unauthenticated: infoStatusCodeHandler, - - codes.Aborted: warningStatusCodeHandler, - codes.DeadlineExceeded: warningStatusCodeHandler, - codes.FailedPrecondition: warningStatusCodeHandler, - codes.OutOfRange: warningStatusCodeHandler, - codes.PermissionDenied: warningStatusCodeHandler, - codes.ResourceExhausted: warningStatusCodeHandler, - codes.Unavailable: warningStatusCodeHandler, - - codes.DataLoss: errorStatusCodeHandler, - codes.Unknown: errorStatusCodeHandler, - codes.Internal: errorStatusCodeHandler, - codes.Unimplemented: errorStatusCodeHandler, + codes.OK: infoStatusCodeHandler, + codes.Aborted: infoStatusCodeHandler, + codes.AlreadyExists: infoStatusCodeHandler, + codes.Canceled: infoStatusCodeHandler, + codes.DataLoss: infoStatusCodeHandler, + codes.DeadlineExceeded: infoStatusCodeHandler, + codes.FailedPrecondition: infoStatusCodeHandler, + codes.InvalidArgument: infoStatusCodeHandler, + codes.NotFound: infoStatusCodeHandler, + codes.OutOfRange: infoStatusCodeHandler, + codes.PermissionDenied: infoStatusCodeHandler, + codes.ResourceExhausted: infoStatusCodeHandler, + codes.Unauthenticated: infoStatusCodeHandler, + codes.Unimplemented: infoStatusCodeHandler, + + codes.Internal: warningStatusCodeHandler, + codes.Unavailable: warningStatusCodeHandler, + codes.Unknown: warningStatusCodeHandler, } - defaultStatusCodeHandler = errorStatusCodeHandler + defaultStatusCodeHandler = infoStatusCodeHandler resultCodeHandlers = map[string]resultCodeHandler{ - "OK": infoResultCodeHandler, - "NOOP": infoResultCodeHandler, - "NOT_FOUND": infoResultCodeHandler, - "ACTION_NOT_FOUND": infoResultCodeHandler, - "INTENT_NOT_FOUND": infoResultCodeHandler, - "NO_VERIFICATION": infoResultCodeHandler, - "EXISTS": infoResultCodeHandler, - "ALREADY_INVITED": infoResultCodeHandler, - "NOT_INVITED": infoResultCodeHandler, - "SENDER_NOT_INVITED": infoResultCodeHandler, - "INVITE_COUNT_EXCEEDED": infoResultCodeHandler, - - "DENIED": warningResultCodeHandler, - "INVALID_ACTION": warningResultCodeHandler, - "INVALID_CODE": warningResultCodeHandler, - "INVALID_INTENT": warningResultCodeHandler, - "INVALID_PHONE_NUMBER": warningResultCodeHandler, - "INVALID_PUSH_TOKEN": warningResultCodeHandler, - "INVALID_RECEIVER_PHONE_NUMBER": warningResultCodeHandler, - "INVALID_TOKEN": warningResultCodeHandler, - "LANG_UNAVAILABLE": warningResultCodeHandler, - "RATE_LIMITED": warningResultCodeHandler, - "SIGNATURE_ERROR": warningResultCodeHandler, + "OK": infoResultCodeHandler, + "NOT_FOUND": infoResultCodeHandler, + + "DENIED": warningResultCodeHandler, } - defaultResultCodeHandler = errorResultCodeHandler + defaultResultCodeHandler = infoResultCodeHandler ) func infoStatusCodeHandler(m *newrelic.Transaction, s *status.Status) { diff --git a/pkg/kikcode/currency.go b/pkg/kikcode/currency.go deleted file mode 100644 index 41724ae7..00000000 --- a/pkg/kikcode/currency.go +++ /dev/null @@ -1,169 +0,0 @@ -package kikcode - -import "github.com/code-payments/code-server/pkg/currency" - -var ( - // Keep in sync with: - // https://github.com/code-payments/code-ios/blob/master/CodeServices/Sources/CodeServices/Models/CurrencyCode.swift#L2 - supportedCurrenies = []currency.Code{ - // Crypto - - currency.KIN, - - // Fiat - - currency.AED, - currency.AFN, - currency.ALL, - currency.AMD, - currency.ANG, - currency.AOA, - currency.ARS, - currency.AUD, - currency.AWG, - currency.AZN, - currency.BAM, - currency.BBD, - currency.BDT, - currency.BGN, - currency.BHD, - currency.BIF, - currency.BMD, - currency.BND, - currency.BOB, - currency.BRL, - currency.BSD, - currency.BTN, - currency.BWP, - currency.BYN, - currency.BZD, - currency.CAD, - currency.CDF, - currency.CHF, - currency.CLP, - currency.CNY, - currency.COP, - currency.CRC, - currency.CUP, - currency.CVE, - currency.CZK, - currency.DJF, - currency.DKK, - currency.DOP, - currency.DZD, - currency.EGP, - currency.ERN, - currency.ETB, - currency.EUR, - currency.FJD, - currency.FKP, - currency.GBP, - currency.GEL, - currency.GHS, - currency.GIP, - currency.GMD, - currency.GNF, - currency.GTQ, - currency.GYD, - currency.HKD, - currency.HNL, - currency.HRK, - currency.HTG, - currency.HUF, - currency.IDR, - currency.ILS, - currency.INR, - currency.IQD, - currency.IRR, - currency.ISK, - currency.JMD, - currency.JOD, - currency.JPY, - currency.KES, - currency.KGS, - currency.KHR, - currency.KMF, - currency.KPW, - currency.KRW, - currency.KWD, - currency.KYD, - currency.KZT, - currency.LAK, - currency.LBP, - currency.LKR, - currency.LRD, - currency.LYD, - currency.MAD, - currency.MDL, - currency.MGA, - currency.MKD, - currency.MMK, - currency.MNT, - currency.MOP, - currency.MRU, - currency.MUR, - currency.MVR, - currency.MWK, - currency.MXN, - currency.MYR, - currency.MZN, - currency.NAD, - currency.NGN, - currency.NIO, - currency.NOK, - currency.NPR, - currency.NZD, - currency.OMR, - currency.PAB, - currency.PEN, - currency.PGK, - currency.PHP, - currency.PKR, - currency.PLN, - currency.PYG, - currency.QAR, - currency.RON, - currency.RSD, - currency.RUB, - currency.RWF, - currency.SAR, - currency.SBD, - currency.SCR, - currency.SDG, - currency.SEK, - currency.SGD, - currency.SHP, - currency.SLL, - currency.SOS, - currency.SRD, - currency.SSP, - currency.STN, - currency.SYP, - currency.SZL, - currency.THB, - currency.TJS, - currency.TMT, - currency.TND, - currency.TOP, - currency.TRY, - currency.TTD, - currency.TWD, - currency.TZS, - currency.UAH, - currency.UGX, - currency.USD, - currency.UYU, - currency.UZS, - currency.VES, - currency.VND, - currency.VUV, - currency.WST, - currency.XAF, - currency.XCD, - currency.XOF, - currency.XPF, - currency.YER, - currency.ZAR, - currency.ZMW, - } -) diff --git a/pkg/kikcode/encoding/Array.h b/pkg/kikcode/encoding/Array.h deleted file mode 100644 index f89e2168..00000000 --- a/pkg/kikcode/encoding/Array.h +++ /dev/null @@ -1,170 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -#ifndef __ARRAY_H__ -#define __ARRAY_H__ - -/* - * Array.h - * zxing - * - * Copyright 2010 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include "Counted.h" - -namespace zxing { - -template class Array : public Counted { -protected: -public: - std::vector values_; - Array() {} - Array(int n) : - Counted(), values_(n, T()) { - } - Array(T const* ts, int n) : - Counted(), values_(ts, ts+n) { - } - Array(T const* ts, T const* te) : - Counted(), values_(ts, te) { - } - Array(T v, int n) : - Counted(), values_(n, v) { - } - Array(std::vector &v) : - Counted(), values_(v) { - } - Array(Array &other) : - Counted(), values_(other.values_) { - } - Array(Array *other) : - Counted(), values_(other->values_) { - } - virtual ~Array() { - } - Array& operator=(const Array &other) { - values_ = other.values_; - return *this; - } - Array& operator=(const std::vector &array) { - values_ = array; - return *this; - } - T const& operator[](int i) const { - return values_[i]; - } - T& operator[](int i) { - return values_[i]; - } - int size() const { - return values_.size(); - } - bool empty() const { - return values_.size() == 0; - } - std::vector const& values() const { - return values_; - } - std::vector& values() { - return values_; - } -}; - -template class ArrayRef : public Counted { -private: -public: - Array *array_; - ArrayRef() : - array_(0) { - } - explicit ArrayRef(int n) : - array_(0) { - reset(new Array (n)); - } - ArrayRef(T *ts, int n) : - array_(0) { - reset(new Array (ts, n)); - } - ArrayRef(Array *a) : - array_(0) { - reset(a); - } - ArrayRef(const ArrayRef &other) : - Counted(), array_(0) { - reset(other.array_); - } - - template - ArrayRef(const ArrayRef &other) : - array_(0) { - reset(static_cast *>(other.array_)); - } - - ~ArrayRef() { - if (array_) { - array_->release(); - } - array_ = 0; - } - - T const& operator[](int i) const { - return (*array_)[i]; - } - - T& operator[](int i) { - return (*array_)[i]; - } - - void reset(Array *a) { - if (a) { - a->retain(); - } - if (array_) { - array_->release(); - } - array_ = a; - } - void reset(const ArrayRef &other) { - reset(other.array_); - } - ArrayRef& operator=(const ArrayRef &other) { - reset(other); - return *this; - } - ArrayRef& operator=(Array *a) { - reset(a); - return *this; - } - - Array& operator*() const { - return *array_; - } - - Array* operator->() const { - return array_; - } - - operator bool () const { - return array_ != 0; - } - bool operator ! () const { - return array_ == 0; - } -}; - -} // namespace zxing - -#endif // __ARRAY_H__ diff --git a/pkg/kikcode/encoding/Counted.h b/pkg/kikcode/encoding/Counted.h deleted file mode 100644 index 41ac5eca..00000000 --- a/pkg/kikcode/encoding/Counted.h +++ /dev/null @@ -1,140 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -#ifndef __COUNTED_H__ -#define __COUNTED_H__ - -/* - * Copyright 2010 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -namespace zxing { - -/* base class for reference-counted objects */ -class Counted { -private: - unsigned int count_; -public: - Counted() : - count_(0) { - } - virtual ~Counted() { - } - Counted *retain() { - count_++; - return this; - } - void release() { - count_--; - if (count_ == 0) { - count_ = 0xDEADF001; - delete this; - } - } - - - /* return the current count for denugging purposes or similar */ - int count() const { - return count_; - } -}; - -/* counting reference to reference-counted objects */ -template class Ref { -private: -public: - T *object_; - explicit Ref(T *o = 0) : - object_(0) { - reset(o); - } - Ref(const Ref &other) : - object_(0) { - reset(other.object_); - } - - template - Ref(const Ref &other) : - object_(0) { - reset(other.object_); - } - - ~Ref() { - if (object_) { - object_->release(); - } - } - - void reset(T *o) { - if (o) { - o->retain(); - } - if (object_ != 0) { - object_->release(); - } - object_ = o; - } - Ref& operator=(const Ref &other) { - reset(other.object_); - return *this; - } - template - Ref& operator=(const Ref &other) { - reset(other.object_); - return *this; - } - Ref& operator=(T* o) { - reset(o); - return *this; - } - template - Ref& operator=(Y* o) { - reset(o); - return *this; - } - - T& operator*() { - return *object_; - } - T* operator->() const { - return object_; - } - operator T*() const { - return object_; - } - - bool operator==(const T* that) { - return object_ == that; - } - bool operator==(const Ref &other) const { - return object_ == other.object_ || *object_ == *(other.object_); - } - template - bool operator==(const Ref &other) const { - return object_ == other.object_ || *object_ == *(other.object_); - } - - bool operator!=(const T* that) { - return !(*this == that); - } - - bool empty() const { - return object_ == 0; - } -}; - -} - -#endif // __COUNTED_H__ diff --git a/pkg/kikcode/encoding/Exception.cpp b/pkg/kikcode/encoding/Exception.cpp deleted file mode 100644 index fa4b98e3..00000000 --- a/pkg/kikcode/encoding/Exception.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -/* - * Exception.cpp - * ZXing - * - * Created by Christian Brunschen on 03/06/2008. - * Copyright 2008-2011 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - - */ - -#include "ZXing.h" -#include "Exception.h" -#include - -using zxing::Exception; - -void Exception::deleteMessage() { - delete [] message; -} - -char const* Exception::copy(char const* msg) { - char* message = 0; - if (msg) { - int l = strlen(msg)+1; - if (l) { - message = new char[l]; - strcpy(message, msg); - } - } - return message; -} diff --git a/pkg/kikcode/encoding/Exception.h b/pkg/kikcode/encoding/Exception.h deleted file mode 100644 index f0df1e5b..00000000 --- a/pkg/kikcode/encoding/Exception.h +++ /dev/null @@ -1,51 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -#ifndef __EXCEPTION_H__ -#define __EXCEPTION_H__ - -/* - * Exception.h - * ZXing - * - * Copyright 2010 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include - -namespace zxing { - -class Exception : public std::exception { -private: - char const* const message; - -public: - Exception() throw() : message(0) {} - Exception(const char* msg) throw() : message(copy(msg)) {} - Exception(Exception const& that) throw() : std::exception(that), message(copy(that.message)) {} - ~Exception() throw() { - if(message) { - deleteMessage(); - } - } - char const* what() const throw() {return message ? message : "";} - -private: - static char const* copy(char const*); - void deleteMessage(); -}; - -} - -#endif // __EXCEPTION_H__ diff --git a/pkg/kikcode/encoding/GenericGF.cpp b/pkg/kikcode/encoding/GenericGF.cpp deleted file mode 100644 index fb74b08d..00000000 --- a/pkg/kikcode/encoding/GenericGF.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -/* - * GenericGF.cpp - * zxing - * - * Created by Lukas Stabe on 13/02/2012. - * Copyright 2012 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include "GenericGF.h" -#include "GenericGFPoly.h" -#include "IllegalArgumentException.h" - -using zxing::GenericGF; -using zxing::GenericGFPoly; -using zxing::Ref; - -Ref GenericGF::AZTEC_DATA_12(new GenericGF(0x1069, 4096, 1)); -Ref GenericGF::AZTEC_DATA_10(new GenericGF(0x409, 1024, 1)); -Ref GenericGF::AZTEC_DATA_6(new GenericGF(0x43, 64, 1)); -Ref GenericGF::AZTEC_PARAM(new GenericGF(0x13, 16, 1)); -Ref GenericGF::QR_CODE_FIELD_256(new GenericGF(0x011D, 256, 0)); -Ref GenericGF::DATA_MATRIX_FIELD_256(new GenericGF(0x012D, 256, 1)); -Ref GenericGF::AZTEC_DATA_8 = DATA_MATRIX_FIELD_256; -Ref GenericGF::MAXICODE_FIELD_64 = AZTEC_DATA_6; - -namespace { - int INITIALIZATION_THRESHOLD = 0; -} - -GenericGF::GenericGF(int primitive_, int size_, int b) - : size(size_), primitive(primitive_), generatorBase(b), initialized(false) { - if (size <= INITIALIZATION_THRESHOLD) { - initialize(); - } -} - -void GenericGF::initialize() { - expTable.resize(size); - logTable.resize(size); - - int x = 1; - - for (int i = 0; i < size; i++) { - expTable[i] = x; - x <<= 1; // x = x * 2; we're assuming the generator alpha is 2 - if (x >= size) { - x ^= primitive; - x &= size-1; - } - } - for (int i = 0; i < size-1; i++) { - logTable[expTable[i]] = i; - } - //logTable[0] == 0 but this should never be used - zero = - Ref(new GenericGFPoly(*this, ArrayRef(new Array(1)))); - zero->getCoefficients()[0] = 0; - one = - Ref(new GenericGFPoly(*this, ArrayRef(new Array(1)))); - one->getCoefficients()[0] = 1; - initialized = true; -} - -void GenericGF::checkInit() { - if (!initialized) { - initialize(); - } -} - -Ref GenericGF::getZero() { - checkInit(); - return zero; -} - -Ref GenericGF::getOne() { - checkInit(); - return one; -} - -Ref GenericGF::buildMonomial(int degree, int coefficient) { - checkInit(); - - if (degree < 0) { - throw IllegalArgumentException("Degree must be non-negative"); - } - if (coefficient == 0) { - return zero; - } - ArrayRef coefficients(new Array(degree + 1)); - coefficients[0] = coefficient; - - return Ref(new GenericGFPoly(*this, coefficients)); -} - -int GenericGF::addOrSubtract(int a, int b) { - return a ^ b; -} - -int GenericGF::exp(int a) { - checkInit(); - return expTable[a]; -} - -int GenericGF::log(int a) { - checkInit(); - if (a == 0) { - throw IllegalArgumentException("cannot give log(0)"); - } - return logTable[a]; -} - -int GenericGF::inverse(int a) { - checkInit(); - if (a == 0) { - throw IllegalArgumentException("Cannot calculate the inverse of 0"); - } - return expTable[size - logTable[a] - 1]; -} - -int GenericGF::multiply(int a, int b) { - checkInit(); - - if (a == 0 || b == 0) { - return 0; - } - - return expTable[(logTable[a] + logTable[b]) % (size - 1)]; - } - -int GenericGF::getSize() { - return size; -} - -int GenericGF::getGeneratorBase() { - return generatorBase; -} diff --git a/pkg/kikcode/encoding/GenericGF.h b/pkg/kikcode/encoding/GenericGF.h deleted file mode 100644 index ca878129..00000000 --- a/pkg/kikcode/encoding/GenericGF.h +++ /dev/null @@ -1,73 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -/* - * GenericGF.h - * zxing - * - * Created by Lukas Stabe on 13/02/2012. - * Copyright 2012 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef GENERICGF_H -#define GENERICGF_H - -#include -#include "Counted.h" - -namespace zxing { - class GenericGFPoly; - - class GenericGF : public Counted { - - private: - std::vector expTable; - std::vector logTable; - Ref zero; - Ref one; - int size; - int primitive; - int generatorBase; - bool initialized; - - void initialize(); - void checkInit(); - - public: - static Ref AZTEC_DATA_12; - static Ref AZTEC_DATA_10; - static Ref AZTEC_DATA_8; - static Ref AZTEC_DATA_6; - static Ref AZTEC_PARAM; - static Ref QR_CODE_FIELD_256; - static Ref DATA_MATRIX_FIELD_256; - static Ref MAXICODE_FIELD_64; - - GenericGF(int primitive, int size, int b); - - Ref getZero(); - Ref getOne(); - int getSize(); - int getGeneratorBase(); - Ref buildMonomial(int degree, int coefficient); - - static int addOrSubtract(int a, int b); - int exp(int a); - int log(int a); - int inverse(int a); - int multiply(int a, int b); - }; -} - -#endif //GENERICGF_H - diff --git a/pkg/kikcode/encoding/GenericGFPoly.cpp b/pkg/kikcode/encoding/GenericGFPoly.cpp deleted file mode 100644 index d9b4548b..00000000 --- a/pkg/kikcode/encoding/GenericGFPoly.cpp +++ /dev/null @@ -1,221 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -/* - * GenericGFPoly.cpp - * zxing - * - * Created by Lukas Stabe on 13/02/2012. - * Copyright 2012 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include "GenericGFPoly.h" -#include "GenericGF.h" -#include "IllegalArgumentException.h" - -using zxing::GenericGFPoly; -using zxing::ArrayRef; -using zxing::Array; -using zxing::Ref; - -// VC++ -using zxing::GenericGF; - -GenericGFPoly::GenericGFPoly(GenericGF &field, - ArrayRef coefficients) - : field_(field) { - if (coefficients->size() == 0) { - throw IllegalArgumentException("need coefficients"); - } - int coefficientsLength = coefficients->size(); - if (coefficientsLength > 1 && coefficients[0] == 0) { - // Leading term must be non-zero for anything except the constant polynomial "0" - int firstNonZero = 1; - while (firstNonZero < coefficientsLength && coefficients[firstNonZero] == 0) { - firstNonZero++; - } - if (firstNonZero == coefficientsLength) { - coefficients_ = field.getZero()->getCoefficients(); - } else { - coefficients_ = ArrayRef(new Array(coefficientsLength-firstNonZero)); - for (int i = 0; i < (int)coefficients_->size(); i++) { - coefficients_[i] = coefficients[i + firstNonZero]; - } - } - } else { - coefficients_ = coefficients; - } -} - -ArrayRef GenericGFPoly::getCoefficients() { - return coefficients_; -} - -int GenericGFPoly::getDegree() { - return coefficients_->size() - 1; -} - -bool GenericGFPoly::isZero() { - return coefficients_[0] == 0; -} - -int GenericGFPoly::getCoefficient(int degree) { - return coefficients_[coefficients_->size() - 1 - degree]; -} - -int GenericGFPoly::evaluateAt(int a) { - if (a == 0) { - // Just return the x^0 coefficient - return getCoefficient(0); - } - - int size = coefficients_->size(); - if (a == 1) { - // Just the sum of the coefficients - int result = 0; - for (int i = 0; i < size; i++) { - result = GenericGF::addOrSubtract(result, coefficients_[i]); - } - return result; - } - int result = coefficients_[0]; - for (int i = 1; i < size; i++) { - result = GenericGF::addOrSubtract(field_.multiply(a, result), coefficients_[i]); - } - return result; -} - -Ref GenericGFPoly::addOrSubtract(Ref other) { - if (!(&field_ == &other->field_)) { - throw IllegalArgumentException("GenericGFPolys do not have same GenericGF field"); - } - if (isZero()) { - return other; - } - if (other->isZero()) { - return Ref(this); - } - - ArrayRef smallerCoefficients = coefficients_; - ArrayRef largerCoefficients = other->getCoefficients(); - if (smallerCoefficients->size() > largerCoefficients->size()) { - ArrayRef temp = smallerCoefficients; - smallerCoefficients = largerCoefficients; - largerCoefficients = temp; - } - - ArrayRef sumDiff(new Array(largerCoefficients->size())); - int lengthDiff = largerCoefficients->size() - smallerCoefficients->size(); - // Copy high-order terms only found in higher-degree polynomial's coefficients - for (int i = 0; i < lengthDiff; i++) { - sumDiff[i] = largerCoefficients[i]; - } - - for (int i = lengthDiff; i < (int)largerCoefficients->size(); i++) { - sumDiff[i] = GenericGF::addOrSubtract(smallerCoefficients[i-lengthDiff], - largerCoefficients[i]); - } - - return Ref(new GenericGFPoly(field_, sumDiff)); -} - -Ref GenericGFPoly::multiply(Ref other) { - if (!(&field_ == &other->field_)) { - throw IllegalArgumentException("GenericGFPolys do not have same GenericGF field"); - } - - if (isZero() || other->isZero()) { - return field_.getZero(); - } - - ArrayRef aCoefficients = coefficients_; - int aLength = aCoefficients->size(); - - ArrayRef bCoefficients = other->getCoefficients(); - int bLength = bCoefficients->size(); - - ArrayRef product(new Array(aLength + bLength - 1)); - for (int i = 0; i < aLength; i++) { - int aCoeff = aCoefficients[i]; - for (int j = 0; j < bLength; j++) { - product[i+j] = GenericGF::addOrSubtract(product[i+j], - field_.multiply(aCoeff, bCoefficients[j])); - } - } - - return Ref(new GenericGFPoly(field_, product)); -} - -Ref GenericGFPoly::multiply(int scalar) { - if (scalar == 0) { - return field_.getZero(); - } - if (scalar == 1) { - return Ref(this); - } - int size = coefficients_->size(); - ArrayRef product(new Array(size)); - for (int i = 0; i < size; i++) { - product[i] = field_.multiply(coefficients_[i], scalar); - } - return Ref(new GenericGFPoly(field_, product)); -} - -Ref GenericGFPoly::multiplyByMonomial(int degree, int coefficient) { - if (degree < 0) { - throw IllegalArgumentException("degree must not be less then 0"); - } - if (coefficient == 0) { - return field_.getZero(); - } - int size = coefficients_->size(); - ArrayRef product(new Array(size+degree)); - for (int i = 0; i < size; i++) { - product[i] = field_.multiply(coefficients_[i], coefficient); - } - return Ref(new GenericGFPoly(field_, product)); -} - -std::vector > GenericGFPoly::divide(Ref other) { - if (!(&field_ == &other->field_)) { - throw IllegalArgumentException("GenericGFPolys do not have same GenericGF field"); - } - if (other->isZero()) { - throw IllegalArgumentException("divide by 0"); - } - - Ref quotient = field_.getZero(); - Ref remainder(new GenericGFPoly(*this)); - - int denominatorLeadingTerm = other->getCoefficient(other->getDegree()); - int inverseDenominatorLeadingTerm = field_.inverse(denominatorLeadingTerm); - - - while (remainder->getDegree() >= other->getDegree() && !remainder->isZero()) { - int degreeDifference = remainder->getDegree() - other->getDegree(); - int scale = field_.multiply(remainder->getCoefficient(remainder->getDegree()), - inverseDenominatorLeadingTerm); - Ref term = other->multiplyByMonomial(degreeDifference, scale); - Ref iterationQuotiont = field_.buildMonomial(degreeDifference, - scale); - quotient = quotient->addOrSubtract(iterationQuotiont); - remainder = remainder->addOrSubtract(term); - } - - std::vector > returnValue(2); - - returnValue[0] = Ref(new GenericGFPoly(*quotient)); - returnValue[1] = Ref(new GenericGFPoly(*remainder)); - return returnValue; -} diff --git a/pkg/kikcode/encoding/GenericGFPoly.h b/pkg/kikcode/encoding/GenericGFPoly.h deleted file mode 100644 index 1b7b6119..00000000 --- a/pkg/kikcode/encoding/GenericGFPoly.h +++ /dev/null @@ -1,56 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -/* - * GenericGFPoly.h - * zxing - * - * Created by Lukas Stabe on 13/02/2012. - * Copyright 2012 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef GENERICGFPOLY_H -#define GENERICGFPOLY_H - -#include -#include "Array.h" -#include "Counted.h" - -namespace zxing { - -class GenericGF; - -class GenericGFPoly : public Counted { -private: - GenericGF &field_; - ArrayRef coefficients_; - -public: - GenericGFPoly(GenericGF &field, ArrayRef coefficients); - ArrayRef getCoefficients(); - int getDegree(); - bool isZero(); - int getCoefficient(int degree); - int evaluateAt(int a); - Ref addOrSubtract(Ref other); - Ref multiply(Ref other); - Ref multiply(int scalar); - Ref multiplyByMonomial(int degree, int coefficient); - std::vector > divide(Ref other); - - -}; - -} - -#endif //GENERICGFPOLY_H diff --git a/pkg/kikcode/encoding/IllegalArgumentException.cpp b/pkg/kikcode/encoding/IllegalArgumentException.cpp deleted file mode 100644 index e635d092..00000000 --- a/pkg/kikcode/encoding/IllegalArgumentException.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/* - * IllegalArgumentException.cpp - * zxing - * - * Created by Christian Brunschen on 06/05/2008. - * Copyright 2008 Google UK. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "IllegalArgumentException.h" - -using zxing::IllegalArgumentException; - -IllegalArgumentException::IllegalArgumentException() : Exception() {} -IllegalArgumentException::IllegalArgumentException(const char *msg) : Exception(msg) {} -IllegalArgumentException::~IllegalArgumentException() throw() {} diff --git a/pkg/kikcode/encoding/IllegalArgumentException.h b/pkg/kikcode/encoding/IllegalArgumentException.h deleted file mode 100644 index f6987cc5..00000000 --- a/pkg/kikcode/encoding/IllegalArgumentException.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef __ILLEGAL_ARGUMENT_EXCEPTION_H__ -#define __ILLEGAL_ARGUMENT_EXCEPTION_H__ - -/* - * IllegalArgumentException.h - * zxing - * - * Copyright 2010 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Exception.h" - -namespace zxing { - -class IllegalArgumentException : public Exception { -public: - IllegalArgumentException(); - IllegalArgumentException(const char *msg); - ~IllegalArgumentException() throw(); -}; - -} - -#endif // __ILLEGAL_ARGUMENT_EXCEPTION_H__ diff --git a/pkg/kikcode/encoding/IllegalStateException.h b/pkg/kikcode/encoding/IllegalStateException.h deleted file mode 100644 index dcd13d70..00000000 --- a/pkg/kikcode/encoding/IllegalStateException.h +++ /dev/null @@ -1,35 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- - -#ifndef __ILLEGAL_STATE_EXCEPTION_H__ -#define __ILLEGAL_STATE_EXCEPTION_H__ - -/* - * Copyright 20011 ZXing authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may illegal use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "ReaderException.h" - -namespace zxing { - -class IllegalStateException : public ReaderException { -public: - IllegalStateException() throw() {} - IllegalStateException(const char *msg) throw() : ReaderException(msg) {} - ~IllegalStateException() throw() {} -}; - -} - -#endif // __ILLEGAL_STATE_EXCEPTION_H__ diff --git a/pkg/kikcode/encoding/ReaderException.h b/pkg/kikcode/encoding/ReaderException.h deleted file mode 100644 index 401abf5e..00000000 --- a/pkg/kikcode/encoding/ReaderException.h +++ /dev/null @@ -1,37 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -#ifndef __READER_EXCEPTION_H__ -#define __READER_EXCEPTION_H__ - -/* - * ReaderException.h - * zxing - * - * Copyright 2010 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Exception.h" - -namespace zxing { - -class ReaderException : public Exception { - public: - ReaderException() throw() {} - ReaderException(char const* msg) throw() : Exception(msg) {} - ~ReaderException() throw() {} -}; - -} - -#endif // __READER_EXCEPTION_H__ diff --git a/pkg/kikcode/encoding/ReedSolomonDecoder.cpp b/pkg/kikcode/encoding/ReedSolomonDecoder.cpp deleted file mode 100644 index a9999b6f..00000000 --- a/pkg/kikcode/encoding/ReedSolomonDecoder.cpp +++ /dev/null @@ -1,174 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -/* - * Created by Christian Brunschen on 05/05/2008. - * Copyright 2008 Google UK. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include -#include "ReedSolomonDecoder.h" -#include "ReedSolomonException.h" -#include "IllegalArgumentException.h" -#include "IllegalStateException.h" - -using std::vector; -using zxing::Ref; -using zxing::ArrayRef; -using zxing::ReedSolomonDecoder; -using zxing::GenericGFPoly; -using zxing::IllegalStateException; - -// VC++ -using zxing::GenericGF; - -ReedSolomonDecoder::ReedSolomonDecoder(Ref field_) : field(field_) {} - -ReedSolomonDecoder::~ReedSolomonDecoder() { -} - -void ReedSolomonDecoder::decode(ArrayRef received, int twoS) { - Ref poly(new GenericGFPoly(*field, received)); - ArrayRef syndromeCoefficients(twoS); - bool noError = true; - for (int i = 0; i < twoS; i++) { - int eval = poly->evaluateAt(field->exp(i + field->getGeneratorBase())); - syndromeCoefficients[syndromeCoefficients->size() - 1 - i] = eval; - if (eval != 0) { - noError = false; - } - } - if (noError) { - return; - } - Ref syndrome(new GenericGFPoly(*field, syndromeCoefficients)); - vector > sigmaOmega = - runEuclideanAlgorithm(field->buildMonomial(twoS, 1), syndrome, twoS); - Ref sigma = sigmaOmega[0]; - Ref omega = sigmaOmega[1]; - ArrayRef errorLocations = findErrorLocations(sigma); - ArrayRef errorMagitudes = findErrorMagnitudes(omega, errorLocations); - for (int i = 0; i < errorLocations->size(); i++) { - int position = received->size() - 1 - field->log(errorLocations[i]); - if (position < 0) { - throw ReedSolomonException("Bad error location"); - } - received[position] = GenericGF::addOrSubtract(received[position], errorMagitudes[i]); - } -} - -vector > ReedSolomonDecoder::runEuclideanAlgorithm(Ref a, - Ref b, - int R) { - // Assume a's degree is >= b's - if (a->getDegree() < b->getDegree()) { - Ref tmp = a; - a = b; - b = tmp; - } - - Ref rLast(a); - Ref r(b); - Ref tLast(field->getZero()); - Ref t(field->getOne()); - - // Run Euclidean algorithm until r's degree is less than R/2 - while (r->getDegree() >= R / 2) { - Ref rLastLast(rLast); - Ref tLastLast(tLast); - rLast = r; - tLast = t; - - // Divide rLastLast by rLast, with quotient q and remainder r - if (rLast->isZero()) { - // Oops, Euclidean algorithm already terminated? - throw ReedSolomonException("r_{i-1} was zero"); - } - r = rLastLast; - Ref q = field->getZero(); - int denominatorLeadingTerm = rLast->getCoefficient(rLast->getDegree()); - int dltInverse = field->inverse(denominatorLeadingTerm); - while (r->getDegree() >= rLast->getDegree() && !r->isZero()) { - int degreeDiff = r->getDegree() - rLast->getDegree(); - int scale = field->multiply(r->getCoefficient(r->getDegree()), dltInverse); - q = q->addOrSubtract(field->buildMonomial(degreeDiff, scale)); - r = r->addOrSubtract(rLast->multiplyByMonomial(degreeDiff, scale)); - } - - t = q->multiply(tLast)->addOrSubtract(tLastLast); - - if (r->getDegree() >= rLast->getDegree()) { - throw IllegalStateException("Division algorithm failed to reduce polynomial?"); - } - } - - int sigmaTildeAtZero = t->getCoefficient(0); - if (sigmaTildeAtZero == 0) { - throw ReedSolomonException("sigmaTilde(0) was zero"); - } - - int inverse = field->inverse(sigmaTildeAtZero); - Ref sigma(t->multiply(inverse)); - Ref omega(r->multiply(inverse)); - vector > result(2); - result[0] = sigma; - result[1] = omega; - return result; -} - -ArrayRef ReedSolomonDecoder::findErrorLocations(Ref errorLocator) { - // This is a direct application of Chien's search - int numErrors = errorLocator->getDegree(); - if (numErrors == 1) { // shortcut - ArrayRef result(new Array(1)); - result[0] = errorLocator->getCoefficient(1); - return result; - } - ArrayRef result(new Array(numErrors)); - int e = 0; - for (int i = 1; i < field->getSize() && e < numErrors; i++) { - if (errorLocator->evaluateAt(i) == 0) { - result[e] = field->inverse(i); - e++; - } - } - if (e != numErrors) { - throw ReedSolomonException("Error locator degree does not match number of roots"); - } - return result; -} - -ArrayRef ReedSolomonDecoder::findErrorMagnitudes(Ref errorEvaluator, ArrayRef errorLocations) { - // This is directly applying Forney's Formula - int s = errorLocations->size(); - ArrayRef result(new Array(s)); - for (int i = 0; i < s; i++) { - int xiInverse = field->inverse(errorLocations[i]); - int denominator = 1; - for (int j = 0; j < s; j++) { - if (i != j) { - int term = field->multiply(errorLocations[j], xiInverse); - int termPlus1 = (term & 0x1) == 0 ? term | 1 : term & ~1; - denominator = field->multiply(denominator, termPlus1); - } - } - result[i] = field->multiply(errorEvaluator->evaluateAt(xiInverse), - field->inverse(denominator)); - if (field->getGeneratorBase() != 0) { - result[i] = field->multiply(result[i], xiInverse); - } - } - return result; -} diff --git a/pkg/kikcode/encoding/ReedSolomonDecoder.h b/pkg/kikcode/encoding/ReedSolomonDecoder.h deleted file mode 100644 index 2d4728dc..00000000 --- a/pkg/kikcode/encoding/ReedSolomonDecoder.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef __REED_SOLOMON_DECODER_H__ -#define __REED_SOLOMON_DECODER_H__ - -/* - * ReedSolomonDecoder.h - * zxing - * - * Copyright 2010 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include "Counted.h" -#include "Array.h" -#include "GenericGFPoly.h" -#include "GenericGF.h" - -namespace zxing { -class GenericGFPoly; -class GenericGF; - -class ReedSolomonDecoder { -private: - Ref field; -public: - ReedSolomonDecoder(Ref fld); - ~ReedSolomonDecoder(); - void decode(ArrayRef received, int twoS); - std::vector > runEuclideanAlgorithm(Ref a, Ref b, int R); - -private: - ArrayRef findErrorLocations(Ref errorLocator); - ArrayRef findErrorMagnitudes(Ref errorEvaluator, ArrayRef errorLocations); -}; -} - -#endif // __REED_SOLOMON_DECODER_H__ diff --git a/pkg/kikcode/encoding/ReedSolomonEncoder.cpp b/pkg/kikcode/encoding/ReedSolomonEncoder.cpp deleted file mode 100644 index d91b390d..00000000 --- a/pkg/kikcode/encoding/ReedSolomonEncoder.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include - -#include -#include "ReedSolomonEncoder.h" -#include "ReedSolomonException.h" -#include "IllegalArgumentException.h" -#include "IllegalStateException.h" - -using std::vector; -using zxing::Ref; -using zxing::ArrayRef; -using zxing::ReedSolomonEncoder; -using zxing::GenericGFPoly; -using zxing::IllegalStateException; - -// VC++ -using zxing::GenericGF; - -ReedSolomonEncoder::ReedSolomonEncoder(GenericGF &field) : field_(field) -{ - ArrayRef initializeArray(new Array (2)); - - initializeArray[0] = 1; - initializeArray[1] = 1; - - cachedGenerators_.push_back(Ref(new GenericGFPoly(field_, initializeArray))); - } - - Ref ReedSolomonEncoder::buildGenerator(int degree) - { - if (degree >= (int)cachedGenerators_.size()) - { - Ref lastGenerator = cachedGenerators_[cachedGenerators_.size() - 1]; - for (int d = cachedGenerators_.size(); d <= degree; d++) { - ArrayRef initializeArray(new Array (2)); - initializeArray[0] = 1; - initializeArray[1] = field_.exp(d - 1); - Ref poly(new GenericGFPoly(field_, initializeArray)); - Ref nextGenerator = lastGenerator->multiply(poly); - cachedGenerators_.push_back(nextGenerator); - lastGenerator = nextGenerator; - } - } - return cachedGenerators_[degree]; - } - - void ReedSolomonEncoder::encode(ArrayRef toEncode, int ecBytes) - { - if (ecBytes == 0) - { - throw new IllegalArgumentException("No error correction bytes"); - } - int dataBytes = toEncode->size() - ecBytes; - if (dataBytes <= 0) - { - throw new IllegalArgumentException("No data bytes provided"); - } - Ref generator = buildGenerator(ecBytes); - ArrayRef infoCoefficients(new Array(dataBytes)); - - for (int i = 0; i < dataBytes; ++i) { - infoCoefficients[i] = toEncode[i]; - } - - //System.arraycopy(toEncode, 0, infoCoefficients, 0, dataBytes); - Ref info(new GenericGFPoly(field_, infoCoefficients)); - info = info->multiplyByMonomial(ecBytes, 1); - - - Ref remainder = info->divide(generator)[1]; - ArrayRef coefficients = remainder->getCoefficients(); - int numZeroCoefficients = ecBytes - coefficients->size(); - - for (int i = 0; i < numZeroCoefficients; i++) { - toEncode[dataBytes + i] = 0; - } - - for (int i = 0; i < coefficients->size(); ++i) { - toEncode[dataBytes + numZeroCoefficients + i] = coefficients[i]; - } - } diff --git a/pkg/kikcode/encoding/ReedSolomonEncoder.h b/pkg/kikcode/encoding/ReedSolomonEncoder.h deleted file mode 100644 index c4068955..00000000 --- a/pkg/kikcode/encoding/ReedSolomonEncoder.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef __REED_SOLOMON_ENCODER_H__ -#define __REED_SOLOMON_ENCODER_H__ - -#include -#include -#include "Counted.h" -#include "Array.h" -#include "GenericGFPoly.h" -#include "GenericGF.h" - -namespace zxing { - class ReedSolomonEncoder { - private: - GenericGF &field_; - std::vector > cachedGenerators_; - - Ref buildGenerator(int degree); - - public: - ReedSolomonEncoder(GenericGF &field); - void encode(ArrayRef toEncode, int ecBytes); - }; -} - -#endif // __REED_SOLOMON_ENCODER_H__ diff --git a/pkg/kikcode/encoding/ReedSolomonException.cpp b/pkg/kikcode/encoding/ReedSolomonException.cpp deleted file mode 100644 index 2414b6fd..00000000 --- a/pkg/kikcode/encoding/ReedSolomonException.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/* - * ReedSolomonException.cpp - * zxing - * - * Created by Christian Brunschen on 06/05/2008. - * Copyright 2008 Google UK. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "ReedSolomonException.h" - -namespace zxing { -ReedSolomonException::ReedSolomonException(const char *msg) throw() : - Exception(msg) { -} -ReedSolomonException::~ReedSolomonException() throw() { -} - -} diff --git a/pkg/kikcode/encoding/ReedSolomonException.h b/pkg/kikcode/encoding/ReedSolomonException.h deleted file mode 100644 index 8b3cfdbc..00000000 --- a/pkg/kikcode/encoding/ReedSolomonException.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef __REED_SOLOMON_EXCEPTION_H__ -#define __REED_SOLOMON_EXCEPTION_H__ - -/* - * ReedSolomonException.h - * zxing - * - * Copyright 2010 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Exception.h" - -namespace zxing { -class ReedSolomonException : public Exception { -public: - ReedSolomonException(const char *msg) throw(); - ~ReedSolomonException() throw(); -}; -} - -#endif // __REED_SOLOMON_EXCEPTION_H__ diff --git a/pkg/kikcode/encoding/ZXing.h b/pkg/kikcode/encoding/ZXing.h deleted file mode 100644 index 0b6918dc..00000000 --- a/pkg/kikcode/encoding/ZXing.h +++ /dev/null @@ -1,133 +0,0 @@ -// -*- mode:c++; tab-width:2; indent-tabs-mode:nil; c-basic-offset:2 -*- -/* - * Copyright 2013 ZXing authors All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#ifndef __ZXING_H_ -#define __ZXING_H_ - -#define ZXING_ARRAY_LEN(v) ((int)(sizeof(v)/sizeof(v[0]))) -#define ZX_LOG_DIGITS(digits) \ - ((digits == 8) ? 3 : \ - ((digits == 16) ? 4 : \ - ((digits == 32) ? 5 : \ - ((digits == 64) ? 6 : \ - ((digits == 128) ? 7 : \ - (-1)))))) - -#ifndef ZXING_DEBUG -#define ZXING_DEBUG 0 -#endif - -namespace zxing { -typedef char byte; -typedef bool boolean; -} - -#include - -#if defined(_WIN32) || defined(_WIN64) - -#include - -namespace zxing { -inline bool isnan(float v) {return _isnan(v) != 0;} -inline bool isnan(double v) {return _isnan(v) != 0;} -inline float nan() {return std::numeric_limits::quiet_NaN();} -} - -#else - -#include - -namespace zxing { -inline bool isnan(float v) {return std::isnan(v);} -inline bool isnan(double v) {return std::isnan(v);} -inline float nan() {return std::numeric_limits::quiet_NaN();} -} - -#endif - -#if ZXING_DEBUG - -#include -#include - -using std::cout; -using std::cerr; -using std::endl; -using std::flush; -using std::string; -using std::ostream; - -#if ZXING_DEBUG_TIMER - -#include - -namespace zxing { - -class DebugTimer { -public: - DebugTimer(char const* string_) : chars(string_) { - gettimeofday(&start, 0); - } - - DebugTimer(std::string const& string_) : chars(0), string(string_) { - gettimeofday(&start, 0); - } - - void mark(char const* string) { - struct timeval end; - gettimeofday(&end, 0); - int diff = - (end.tv_sec - start.tv_sec)*1000*1000+(end.tv_usec - start.tv_usec); - - cerr << diff << " " << string << '\n'; - } - - void mark(std::string string) { - mark(string.c_str()); - } - - ~DebugTimer() { - if (chars) { - mark(chars); - } else { - mark(string.c_str()); - } - } - -private: - char const* const chars; - std::string string; - struct timeval start; -}; - -} - -#define ZXING_TIME(string) DebugTimer __timer__ (string) -#define ZXING_TIME_MARK(string) __timer__.mark(string) - -#endif - -#endif // ZXING_DEBUG - -#ifndef ZXING_TIME -#define ZXING_TIME(string) (void)0 -#endif -#ifndef ZXING_TIME_MARK -#define ZXING_TIME_MARK(string) (void)0 -#endif - -#endif diff --git a/pkg/kikcode/encoding/encoding.go b/pkg/kikcode/encoding/encoding.go deleted file mode 100644 index c2a80fab..00000000 --- a/pkg/kikcode/encoding/encoding.go +++ /dev/null @@ -1,49 +0,0 @@ -package encoding - -// todo: There are some linking issues if we don't have all the C/C++ files in -// the current directory, but this makes the package extremely ugly. - -// #cgo CXXFLAGS: -I${SRCDIR} -// #cgo CXXFLAGS: -std=c++11 -// #include "kikcode_wrapper.h" -import "C" -import ( - "unsafe" - - "github.com/pkg/errors" -) - -const ( - payloadSize = 20 - encodedPayloadSize = 35 -) - -func Encode(payload []byte) ([]byte, error) { - if len(payload) != payloadSize { - return nil, errors.Errorf("payload value must be a byte array of size %d", payloadSize) - } - - charArray := C.CString(string(payload)) - defer C.free(unsafe.Pointer(charArray)) - - encoded := C.kikcode_encode( - charArray, - C.int(len(payload)), - ) - return []byte(C.GoStringN(encoded, encodedPayloadSize)), nil -} - -func Decode(encoded []byte) ([]byte, error) { - if len(encoded) != encodedPayloadSize { - return nil, errors.Errorf("encoded value must be a byte array of size %d", encodedPayloadSize) - } - - charArray := C.CString(string(encoded)) - defer C.free(unsafe.Pointer(charArray)) - - decoded := C.kikcode_decode( - charArray, - C.int(len(encoded)), - ) - return []byte(C.GoStringN(decoded, payloadSize)), nil -} diff --git a/pkg/kikcode/encoding/encoding_test.go b/pkg/kikcode/encoding/encoding_test.go deleted file mode 100644 index cc2ab88f..00000000 --- a/pkg/kikcode/encoding/encoding_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package encoding - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEncodingRoundTrip(t *testing.T) { - key := []byte{ - 0x6D, 0x70, 0x72, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x40, 0x71, - 0xD8, 0x9E, 0x81, 0x34, 0x63, - 0x06, 0xA0, 0x35, 0xA6, 0x83, - } - encoded, err := Encode(key) - require.NoError(t, err) - - decoded, err := Decode(encoded) - require.NoError(t, err) - - assert.EqualValues(t, key, decoded) -} diff --git a/pkg/kikcode/encoding/kikcode_constants.h b/pkg/kikcode/encoding/kikcode_constants.h deleted file mode 100644 index f7a98f01..00000000 --- a/pkg/kikcode/encoding/kikcode_constants.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef __KIKCODE_CONSTANTS_H__ -#define __KIKCODE_CONSTANTS_H__ - -#define KIK_CODE_TOTAL_BYTE_COUNT 35 - -#endif // __KIKCODE_CONSTANTS_H__ diff --git a/pkg/kikcode/encoding/kikcode_encoding.cpp b/pkg/kikcode/encoding/kikcode_encoding.cpp deleted file mode 100644 index 77350acf..00000000 --- a/pkg/kikcode/encoding/kikcode_encoding.cpp +++ /dev/null @@ -1,472 +0,0 @@ -#include -#include -#include - -#include "kikcode_encoding.h" - -#include "ReedSolomonEncoder.h" -#include "ReedSolomonDecoder.h" -#include "ReedSolomonException.h" -#include "IllegalArgumentException.h" -#include "IllegalStateException.h" - -using namespace std; -using namespace zxing; - -size_t KikCode::writeByte(unsigned char *out_data, size_t offset, unsigned char value) -{ - out_data[offset] = (uint8_t)(value & 0xff); - - return offset + 1; -} - -size_t KikCode::writeShort(unsigned char *out_data, size_t offset, unsigned short value) -{ - out_data[offset] = (uint8_t)(value & 0xff); - out_data[offset + 1] = (uint8_t)((value & 0xff00) >> 8); - - return offset + 2; -} - -size_t KikCode::writeInt(unsigned char *out_data, size_t offset, unsigned int value) -{ - out_data[offset] = (uint8_t)(value & 0xff); - out_data[offset + 1] = (uint8_t)((value & 0xff00) >> 8); - out_data[offset + 2] = (uint8_t)((value & 0xff0000) >> 16); - out_data[offset + 3] = (uint8_t)((value & 0xff000000) >> 24); - - return offset + 4; -} - -size_t KikCode::writeLong(unsigned char *out_data, size_t offset, unsigned long long value) -{ - uint32_t high_dword = (uint32_t)(value >> 32); - - out_data[offset] = (uint8_t)(value & 0xff); - out_data[offset + 1] = (uint8_t)((value & 0xff00) >> 8); - out_data[offset + 2] = (uint8_t)((value & 0xff0000) >> 16); - out_data[offset + 3] = (uint8_t)((value & 0xff000000) >> 24); - out_data[offset + 4] = (uint8_t)(high_dword & 0xff); - out_data[offset + 5] = (uint8_t)((high_dword & 0xff00) >> 8); - out_data[offset + 6] = (uint8_t)((high_dword & 0xff0000) >> 16); - out_data[offset + 7] = (uint8_t)((high_dword & 0xff000000) >> 24); - - return offset + 8; -} - -unsigned char KikCode::readByte(unsigned char *data, size_t offset) -{ - return data[offset]; -} - -unsigned short KikCode::readShort(unsigned char *data, size_t offset) -{ - return data[offset] | (data[offset + 1] << 8); -} - -unsigned int KikCode::readInt(unsigned char *data, size_t offset) -{ - return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24); -} - -unsigned long long KikCode::readLong(unsigned char *data, size_t offset) -{ - unsigned int low = data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24); - unsigned int high = data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) | (data[offset + 7] << 24); - - return (unsigned long long)low | ((unsigned long long)high << 32); -} - -void KikCode::decode(uint8_t *data) -{ - // type - type_ = (KikCode::Type)(data[0] & 0x1f); - - // colour code - int colour = (data[0] & 0xe0) >> 5; - colour |= (data[1] & 0x07) << 3; - - colour_ = (KikCode::Colour)colour; - - // extra - extra_ = (data[1] & 0xf8) >> 3; -} - -KikCode::KikCode(KikCode::Type type, KikCode::Colour colour) -: type_(type) -, colour_(colour) -, extra_(0) -{ -} - -KikCode::~KikCode() -{ -} - -KikCode::Colour KikCode::colour() const -{ - return colour_; -} - -KikCode::Type KikCode::type() const -{ - return type_; -} - -uint8_t KikCode::extra() const -{ - return extra_; -} - -KikCode *KikCode::parse(const uint8_t *data) -{ - uint8_t data_section[KIK_CODE_ALL_BYTE_COUNT]; - - ArrayRef codeword_ints(KIK_CODE_ALL_BYTE_COUNT); - - unsigned char reordered_data[KIK_CODE_ALL_BYTE_COUNT]; - - // put the ECC back on the end - for (size_t i = 0; i < KIK_CODE_ECC_BYTE_COUNT; ++i) { - reordered_data[i + KIK_CODE_DATA_BYTE_COUNT] = data[i]; - } - - for (size_t i = KIK_CODE_ECC_BYTE_COUNT; i < KIK_CODE_ALL_BYTE_COUNT; ++i) { - reordered_data[i - KIK_CODE_ECC_BYTE_COUNT] = data[i]; - } - - for (size_t i = 0; i < KIK_CODE_ALL_BYTE_COUNT; ++i) { - codeword_ints[i] = reordered_data[i] & 0xff; - } - - ReedSolomonDecoder rs_decoder(GenericGF::QR_CODE_FIELD_256); - - try { - rs_decoder.decode(codeword_ints, KIK_CODE_ECC_BYTE_COUNT-1); - } - catch (ReedSolomonException const &ignored) { - (void)ignored; - return nullptr; - } - catch (IllegalArgumentException const &ignored) { - (void)ignored; - return nullptr; - } - catch (IllegalStateException const &ignored) { - (void)ignored; - return nullptr; - } - - for (size_t i = 0; i < KIK_CODE_ALL_BYTE_COUNT; ++i) { - data_section[i] = (uint8_t)codeword_ints[i]; - } - - // type - uint8_t type = data_section[0] & 0x1f; // lower 5 bits - - // colour code - uint8_t colour_code = (data_section[0] & 0xe0) >> 5; // upper 3 bits - - colour_code |= (data_section[1] & 0x07) << 5; // lower 3 bits - - KikCode *kik_code_result = nullptr; - - switch (type) { - case 1: - kik_code_result = new UsernameKikCode((Colour)colour_code); - break; - case 2: - kik_code_result = new RemoteKikCode((Colour)colour_code); - break; - case 3: - kik_code_result = new GroupKikCode((Colour)colour_code); - break; - } - - if (kik_code_result) { - kik_code_result->decode(data_section); - } - - return kik_code_result; -} - -void KikCode::encode(uint8_t *out_data) -{ - // type - out_data[0] = (uint32_t)type() & 0x1f; // lower 5 bits - - // colour code - out_data[0] |= ((uint32_t)colour() << 5) & 0xe0; // upper 3 bits - out_data[1] = ((uint32_t)colour() >> 3) & 0x07; // lower 3 bits - - // extra - out_data[1] |= ((uint32_t)extra() << 3) & 0xf8; // upper 5 bits - - ArrayRef codeword_ints(KIK_CODE_ALL_BYTE_COUNT); - - for (size_t i = 0; i < KIK_CODE_DATA_BYTE_COUNT; ++i) { - codeword_ints[i] = out_data[i] & 0xff; - } - - // apply error correction - ReedSolomonEncoder rs_encoder(*GenericGF::QR_CODE_FIELD_256); - - try { - rs_encoder.encode(codeword_ints, KIK_CODE_ECC_BYTE_COUNT - 1); - } - catch (ReedSolomonException const &ignored) { - (void)ignored; - throw invalid_argument("Error correction error"); - } - catch (IllegalArgumentException const &ignored) { - (void)ignored; - throw invalid_argument("Error correction error"); - } - catch (IllegalStateException const &ignored) { - (void)ignored; - throw invalid_argument("Error correction error"); - } - - // move the ECC to the front of the data - for (size_t i = 0; i < KIK_CODE_ECC_BYTE_COUNT; ++i) { - out_data[i] = (uint8_t)codeword_ints[i + KIK_CODE_DATA_BYTE_COUNT]; - } - - for (size_t i = 0; i < KIK_CODE_DATA_BYTE_COUNT; ++i) { - out_data[i + KIK_CODE_ECC_BYTE_COUNT] = (uint8_t)codeword_ints[i]; - } -} - -UsernameKikCode::UsernameKikCode(Colour colour) -: KikCode(KikCode::Type::Username, colour) -{ -} - -void UsernameKikCode::decode(uint8_t *data_section) -{ - KikCode::decode(data_section); - - char username[32]; - size_t username_length = extra() + 2; - - if (username_length > 24) { - throw invalid_argument("Username too long"); - } - - // See https://github.com/kikinteractive/kik-product/wiki/Scan-Code#username - // for codepoints and 6-bit encoding - for (size_t i = 0; i < username_length; ++i) { - // offset is computed as the position in the 6-bit encoded input bytes - // based on the position in the 8-bit destination - - // the username starts 2 bytes from the front of the data section so always add 2. - // For every character in the destination username, we are effectively advancing 3/4 - // positions in the input bytes, we can't do this because you can't move to intermediate - // bit positions so we find the start of the nearest 3-byte sequence and then use (i % 4) - // to determine which position in the 6-bit encoding we are looking at currently - int offset = (i - (i % 4)) * 3 / 4 + 2; - int codepoint = 0; - char ch = '\0'; - - // every 4 characters in the username maps to a sequence of 3 packed bytes - // in the 6-bit encoding - // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 - // 0 - 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 = 0x3f - // 1 - 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 = 0xc0 >> 6 | 0x0f << 2 - // 2 - 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 = 0xf0 >> 4 | 0x03 << 4 - // 3 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 = 0xfc >> 2 - - switch (i % 4) { - case 0: - codepoint = (data_section[offset] & 0x3f); - break; - case 1: - codepoint = ((data_section[offset] & 0xc0) >> 6) | ((data_section[offset + 1] & 0x0f) << 2); - break; - case 2: - codepoint = ((data_section[offset + 1] & 0xf0) >> 4) | ((data_section[offset + 2] & 0x03) << 4); - break; - case 3: - codepoint = (data_section[offset + 2] & 0xfc) >> 2; - break; - } - - if (codepoint < 26) { - ch = 'A' + codepoint; - } - else if (codepoint < 52) { - ch = 'a' + (codepoint - 26); - } - else if (codepoint < 62) { - ch = '0' + (codepoint - 52); - } - else if (codepoint == 62) { - ch = '.'; - } - else if (codepoint == 63) { - ch = '_'; - } - else { - throw invalid_argument("Unknown codepoint"); - } - - username[i] = ch; - } - - // null-terminate the username - username[username_length] = '\0'; - - username_ = std::string(username, username_length); - nonce_ = data_section[20] | ((int)data_section[21] << 8); -} - -UsernameKikCode::UsernameKikCode(const std::string &username, uint16_t nonce, Colour colour) -: KikCode(KikCode::Type::Username, colour) -, username_(username) -, nonce_(nonce) -{ -} - -void UsernameKikCode::encode(uint8_t *out_data) -{ - memset(out_data, 0, KIK_CODE_DATA_BYTE_COUNT); - - extra_ = username_.length() - 2; - - // encode 6-bit username section - size_t i = 0; - if (username_.length() < 2) { - throw invalid_argument("Username too short"); - } - else if (username_.length() > 24) { - throw invalid_argument("Username too long"); - } - - int offset = 0; - - // See UsernameKikCode::decode for encoding reference - for (size_t l = username_.length(); i < l; ++i) { - offset = (i - (i % 4)) * 3 / 4 + 2; - - char ch = username_[i]; - int value = 0; - - if (ch >= 'A' && ch <= 'Z') { - value = (ch - 'A'); - } - else if (ch >= 'a' && ch <= 'z') { - value = (ch - 'a') + 26; - } - else if (ch >= '0' && ch <= '9') { - value = (ch - '0') + 52; - } - else if (ch == '.') { - value = 62; - } - else if (ch == '_') { - value = 63; - } - else { - throw invalid_argument("Invalid character"); - return; - } - - switch (i % 4) { - case 0: - out_data[offset] = (value & 0x3f); - break; - case 1: - out_data[offset] |= (value & 0x03) << 6; - out_data[offset+1] = (value & 0x3c) >> 2; - break; - case 2: - out_data[offset+1] |= (value & 0x0f) << 4; - out_data[offset+2] = (value & 0x30) >> 4; - break; - case 3: - out_data[offset+2] |= (value & 0x3f) << 2; - break; - } - } - - for (offset += 3; offset < 20; ++offset) { - out_data[offset] = 0xaa ^ offset; - } - - writeShort(out_data, 20, nonce_); - - // finish the encoding and write the error correction - KikCode::encode(out_data); -} - -string UsernameKikCode::username() const -{ - return username_; -} - -uint16_t UsernameKikCode::nonce() const -{ - return nonce_; -} - -RemoteKikCode::RemoteKikCode(Colour colour) -: KikCode(KikCode::Type::Remote, colour) -{ -} - -RemoteKikCode::RemoteKikCode(const std::string &payload, Colour colour) -: KikCode(KikCode::Type::Remote, colour) -, payload_(payload) -{ -} - -void RemoteKikCode::decode(uint8_t *data_section) -{ - KikCode::decode(data_section); - - payload_ = string(reinterpret_cast(data_section + 2), KIK_CODE_PAYLOAD_BYTE_COUNT); -} - -void RemoteKikCode::encode(uint8_t *out_data) -{ - memcpy(out_data + 2, payload_.c_str(), KIK_CODE_PAYLOAD_BYTE_COUNT); - - // finish the encoding and write the error correction - KikCode::encode(out_data); -} - -std::string RemoteKikCode::payload() const -{ - return payload_; -} - -GroupKikCode::GroupKikCode(Colour colour) -: KikCode(KikCode::Type::Group, colour) -{ -} - -GroupKikCode::GroupKikCode(const std::string &invite_code, Colour colour) -: KikCode(KikCode::Type::Group, colour) -, invite_code_(invite_code) -{ -} - -void GroupKikCode::decode(uint8_t *data_section) -{ - KikCode::decode(data_section); - - invite_code_ = string(reinterpret_cast(data_section + 2), KIK_CODE_PAYLOAD_BYTE_COUNT); -} - -void GroupKikCode::encode(uint8_t *out_data) -{ - memcpy(out_data + 2, invite_code_.c_str(), KIK_CODE_PAYLOAD_BYTE_COUNT); - - // finish the encoding and write the error correction - KikCode::encode(out_data); -} - -std::string GroupKikCode::inviteCode() const -{ - return invite_code_; -} diff --git a/pkg/kikcode/encoding/kikcode_encoding.h b/pkg/kikcode/encoding/kikcode_encoding.h deleted file mode 100644 index 5932242e..00000000 --- a/pkg/kikcode/encoding/kikcode_encoding.h +++ /dev/null @@ -1,125 +0,0 @@ -#ifndef __KIKCODE_ENCODING_H__ -#define __KIKCODE_ENCODING_H__ - -#include - -// data constants -#define KIK_CODE_BYTE_COUNT (312/8) -#define KIK_CODE_ALL_BYTE_COUNT (280/8) -#define KIK_CODE_DATA_BYTE_COUNT (176/8) -#define KIK_CODE_ECC_BYTE_COUNT (104/8) -#define KIK_CODE_PAYLOAD_BYTE_COUNT (160/8) - -class KikCode { -public: - enum class Colour { - Default = 0, - TestFive = 5, - TestEleven = 11, - TestFourteen = 14, - - }; - - enum class Type { - Username = 1, - Remote = 2, - Group = 3 - }; - -protected: - Type type_; - Colour colour_; - uint8_t extra_; - - size_t writeByte(unsigned char *out_data, size_t offset, unsigned char value); - - size_t writeShort(unsigned char *out_data, size_t offset, unsigned short value); - - size_t writeInt(unsigned char *out_data, size_t offset, unsigned int value); - - size_t writeLong(unsigned char *out_data, size_t offset, unsigned long long value); - - unsigned char readByte(unsigned char *data, size_t offset); - - unsigned short readShort(unsigned char *data, size_t offset); - - unsigned int readInt(unsigned char *data, size_t offset); - - unsigned long long readLong(unsigned char *data, size_t offset); - - virtual void decode(uint8_t *data_section); - -public: - KikCode(Type type, Colour colour); - - virtual ~KikCode(); - - Colour colour() const; - Type type() const; - uint8_t extra() const; - - static KikCode *parse(const uint8_t *data); - - virtual void encode(uint8_t *out_data); -}; - -class UsernameKikCode : public KikCode { -private: - friend class KikCode; - - std::string username_; - uint16_t nonce_; - - UsernameKikCode(KikCode::Colour colour); - -protected: - virtual void decode(uint8_t *data_section); - -public: - UsernameKikCode(const std::string &username, uint16_t nonce, KikCode::Colour colour); - - virtual void encode(uint8_t *out_data); - - std::string username() const; - uint16_t nonce() const; -}; - -class RemoteKikCode : public KikCode { -private: - friend class KikCode; - - std::string payload_; - - RemoteKikCode(KikCode::Colour colour); - -protected: - virtual void decode(uint8_t *data_section); - -public: - RemoteKikCode(const std::string &payload, KikCode::Colour colour); - - virtual void encode(uint8_t *out_data); - - std::string payload() const; -}; - -class GroupKikCode : public KikCode { -private: - friend class KikCode; - - std::string invite_code_; - - GroupKikCode(KikCode::Colour colour); - -protected: - virtual void decode(uint8_t *data_section); - -public: - GroupKikCode(const std::string &payload, KikCode::Colour colour); - - virtual void encode(uint8_t *out_data); - - std::string inviteCode() const; -}; - -#endif // __KIKCODE_ENCODING_H__ diff --git a/pkg/kikcode/encoding/kikcode_wrapper.cpp b/pkg/kikcode/encoding/kikcode_wrapper.cpp deleted file mode 100644 index ec15844d..00000000 --- a/pkg/kikcode/encoding/kikcode_wrapper.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include -#include - -#include "kikcode_wrapper.h" -#include "kikcodes.h" - -#define TOTAL_BYTE_COUNT 39 -#define MAIN_BYTE_COUNT 35 -#define DATA_BYTE_COUNT 22 -#define PAYLOAD_BYTE_COUNT 20 -#define ECC_BYTE_COUNT 13 -#define ZERO_BYTES { 0 } - -extern "C" { - -char* kikcode_encode(const char* data, int dataSize) { - std::vector dataVector(data, data + dataSize); - static unsigned char outData[MAIN_BYTE_COUNT] = ZERO_BYTES; - - char bytes[PAYLOAD_BYTE_COUNT] = ZERO_BYTES; - memcpy(bytes, dataVector.data(), dataVector.size()); - - kikCodeEncodeRemote(outData, (unsigned char *)bytes, 0); - - return (char*)outData; -} - -char* kikcode_decode(const char* data, int dataSize) { - std::vector dataVector(data, data + dataSize); - KikCodePayload payload; - unsigned int type; - unsigned int color; - kikCodeDecode((unsigned char *)dataVector.data(), &type, &payload, &color); - - // Trim any tail zero bytes at the tail - unsigned char *bytes = payload.group.invite_code; - int length = sizeof(payload.group.invite_code); - while (length > 0 && bytes[length - 1] == 0x0) { - length--; - } - - // Use a static buffer to store the result - static unsigned char* output[MAIN_BYTE_COUNT] = {0}; - memcpy(output, bytes, length); - - return (char*)output; -} - -} // extern "C" \ No newline at end of file diff --git a/pkg/kikcode/encoding/kikcode_wrapper.h b/pkg/kikcode/encoding/kikcode_wrapper.h deleted file mode 100644 index e52cdadf..00000000 --- a/pkg/kikcode/encoding/kikcode_wrapper.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef KIKCODES_WRAPPER_H -#define KIKCODES_WRAPPER_H - -#include "stdlib.h" - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -char* kikcode_encode(const char* data, int dataSize); -char* kikcode_decode(const char* data, int dataSize); - -#ifdef __cplusplus -} -#endif // __cplusplus - -#endif // KIKCODES_WRAPPER_H \ No newline at end of file diff --git a/pkg/kikcode/encoding/kikcodes.cpp b/pkg/kikcode/encoding/kikcodes.cpp deleted file mode 100644 index c8c5d5df..00000000 --- a/pkg/kikcode/encoding/kikcodes.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "kikcodes.h" -#include "kikcode_encoding.h" - -#include -#include - -using namespace std; - -int kikCodeEncodeUsername( - unsigned char *out_data, - const char *username, - const unsigned int username_length, - const unsigned short nonce, - const unsigned int colour_code) -{ - UsernameKikCode kik_code(string(username, username_length), nonce, (KikCode::Colour)colour_code); - - kik_code.encode(out_data); - - return KIK_CODE_RESULT_SUCCESS; -} - -int kikCodeEncodeGroup( - unsigned char *out_data, - const unsigned char *invite_code, - const unsigned int colour_code) -{ - GroupKikCode kik_code(string((const char *)invite_code, 20), (KikCode::Colour)colour_code); - - kik_code.encode(out_data); - - return KIK_CODE_RESULT_SUCCESS; -} - -int kikCodeEncodeRemote( - unsigned char *out_data, - const unsigned char *key, - const unsigned int colour_code) -{ - RemoteKikCode kik_code(string((const char *)key, 20), (KikCode::Colour)colour_code); - - kik_code.encode(out_data); - - return KIK_CODE_RESULT_SUCCESS; -} - -int kikCodeDecode( - const unsigned char *data, - unsigned int *out_type, - KikCodePayload *out_payload, - unsigned int *out_colour_code) -{ - KikCode *kik_code = KikCode::parse(data); - - if (!kik_code) { - return KIK_CODE_RESULT_ERROR; - } - - *out_type = (unsigned int)kik_code->type(); - *out_colour_code = (unsigned int)kik_code->colour(); - - switch (kik_code->type()) { - case KikCode::Type::Username: { - UsernameKikCode *username_code = (UsernameKikCode *)kik_code; - - string username = username_code->username(); - - memcpy(out_payload->username.username, username.c_str(), username.length()); - - out_payload->username.username[username.length()] = '\0'; - - out_payload->username.username_length = username.length(); - out_payload->username.nonce = username_code->nonce(); - - break; - } - case KikCode::Type::Remote: { - RemoteKikCode *remote_code = (RemoteKikCode *)kik_code; - - memcpy(out_payload->remote.payload, remote_code->payload().c_str(), 20); - - break; - } - case KikCode::Type::Group: { - GroupKikCode *group_code = (GroupKikCode *)kik_code; - - memcpy(out_payload->group.invite_code, group_code->inviteCode().c_str(), 20); - - break; - } - } - - delete kik_code; - - return KIK_CODE_RESULT_SUCCESS; -} diff --git a/pkg/kikcode/encoding/kikcodes.h b/pkg/kikcode/encoding/kikcodes.h deleted file mode 100644 index 6c56941b..00000000 --- a/pkg/kikcode/encoding/kikcodes.h +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef __KIKCODES_H__ -#define __KIKCODES_H__ - -#define KIK_CODE_RESULT_SUCCESS 0 -#define KIK_CODE_RESULT_ERROR 1 - -extern "C" { - - union KikCodePayload { - struct { - char username[32]; - unsigned int username_length; - unsigned short nonce; - } username; - - struct { - unsigned char invite_code[20]; - } group; - - struct { - unsigned char payload[20]; - } remote; - }; - - // username codes - int kikCodeEncodeUsername( - unsigned char *out_data, - const char *username, - const unsigned int username_length, - const unsigned short nonce, - const unsigned int colour_code); - - // group codes - int kikCodeEncodeGroup( - unsigned char *out_data, - const unsigned char *invite_code, - const unsigned int colour_code); - - // remote codes - int kikCodeEncodeRemote( - unsigned char *out_data, - const unsigned char *key, - const unsigned int colour_code); - - int kikCodeDecode( - const unsigned char *data, - unsigned int *out_type, - KikCodePayload *out_payload, - unsigned int *out_colour_code); - - } - -#endif // __KIKCODES_H__ diff --git a/pkg/kikcode/payload.go b/pkg/kikcode/payload.go deleted file mode 100644 index 14613bd6..00000000 --- a/pkg/kikcode/payload.go +++ /dev/null @@ -1,208 +0,0 @@ -package kikcode - -import ( - "crypto/ed25519" - "crypto/rand" - "slices" - - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/currency" - "github.com/code-payments/code-server/pkg/kikcode/encoding" -) - -/* - -Layout 0: Cash - - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - | T | Amount | Nonce | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - - (T) Type (1 byte) - - The first byte of the data in all Code scan codes is reserved for the scan - code type. This field indicates which type of scan code data is contained - in the scan code. The expected format for each type is outlined below. - - Kin Amount in Quarks (8 bytes) - - This field indicates the number of quarks the payment is for. It should be - represented as a 64-bit unsigned integer. - - Nonce (11 bytes) - - This field is an 11-byte randomly-generated nonce. It should be regenerated - each time a new payment is initiated. - - Layout 1: Gift Card - - Same as layout 0. - - Layout 2: Payment Request - - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - | T | C | Fiat | Nonce | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - - (T) Type (1 byte) - - The first byte of the data in all Code scan codes is reserved for the scan - code type. This field indicates which type of scan code data is contained - in the scan code. The expected format for each type is outlined below. - - (C) Currency Code (1 bytes) - - This field indicates the currency code for the fiat amount. The value is an - encoded index less than 255 that maps to a currency code in CurrencyCode.swift - - Fiat Amount (7 bytes) - - This field indicates the fiat amount the payment is for. It should represent the - value multiplied by 100 in a 7 byte buffer. - - Nonce (11 bytes) - - This field is an 11-byte randomly-generated nonce. It should be regenerated - each time a new payment is initiated. - -*/ - -type Kind uint8 - -const ( - Cash Kind = iota - GiftCard - PaymentRequest -) - -const ( - typeSize = 1 - amountSize = 8 - nonceSize = 11 - payloadSize = 20 -) - -type IdempotencyKey [nonceSize]byte - -type Payload struct { - kind Kind - amountBuffer amountBuffer - nonce IdempotencyKey -} - -func NewPayloadFromKinAmount(kind Kind, quarks uint64, nonce IdempotencyKey) *Payload { - return &Payload{ - kind: kind, - amountBuffer: newKinAmountBuffer(quarks), - nonce: nonce, - } -} - -func NewPayloadFromFiatAmount(kind Kind, currency currency.Code, amount float64, nonce IdempotencyKey) (*Payload, error) { - amountBuffer, err := newFiatAmountBuffer(currency, amount) - if err != nil { - return nil, err - } - - return &Payload{ - kind: kind, - amountBuffer: amountBuffer, - nonce: nonce, - }, nil -} - -func (p *Payload) ToBytes() []byte { - var buffer [payloadSize]byte - buffer[0] = byte(p.kind) - - amountBuffer := p.amountBuffer.ToBytes() - for i := 0; i < amountSize; i++ { - buffer[i+typeSize] = amountBuffer[i] - } - - for i := 0; i < nonceSize; i++ { - buffer[i+typeSize+amountSize] = p.nonce[i] - } - - return buffer[:] -} - -func (p *Payload) ToQrCodeDescription(dimension float64) (*Description, error) { - viewPayload, err := encoding.Encode(p.ToBytes()) - if err != nil { - return nil, err - } - - kikCodePayload := CreateKikCodePayload(viewPayload) - - return GenerateDescription(dimension, kikCodePayload) -} - -func (p *Payload) GetIdempotencyKey() IdempotencyKey { - return p.nonce -} - -func (p *Payload) ToRendezvousKey() ed25519.PrivateKey { - return DeriveRendezvousPrivateKey(p) -} - -func GenerateRandomIdempotencyKey() IdempotencyKey { - var buffer [nonceSize]byte - rand.Read(buffer[:]) - return buffer -} - -type amountBuffer interface { - ToBytes() [amountSize]byte -} - -type kinAmountBuffer struct { - quarks uint64 -} - -func newKinAmountBuffer(quarks uint64) amountBuffer { - return &kinAmountBuffer{ - quarks: quarks, - } -} - -func (b *kinAmountBuffer) ToBytes() [amountSize]byte { - var buffer [amountSize]byte - for i := 0; i < amountSize; i++ { - buffer[i] = byte(b.quarks >> uint64(8*i) & uint64(0xFF)) - } - return buffer -} - -type fiatAmountBuffer struct { - currency currency.Code - amount float64 -} - -func newFiatAmountBuffer(currency currency.Code, amount float64) (amountBuffer, error) { - index := slices.Index(supportedCurrenies, currency) - if index < 0 { - return nil, errors.Errorf("%s currency is not supported", currency) - } - - return &fiatAmountBuffer{ - currency: currency, - amount: amount, - }, nil -} - -func (b *fiatAmountBuffer) ToBytes() [amountSize]byte { - var buffer [amountSize]byte - - buffer[0] = byte(slices.Index(supportedCurrenies, b.currency)) - - amountToSerialize := uint64(b.amount * 100) - for i := 1; i < amountSize; i++ { - buffer[i] = byte(amountToSerialize >> uint64(8*(i-1)) & uint64(0xFF)) - } - - return buffer -} diff --git a/pkg/kikcode/qr.go b/pkg/kikcode/qr.go deleted file mode 100644 index 3e45fc39..00000000 --- a/pkg/kikcode/qr.go +++ /dev/null @@ -1,179 +0,0 @@ -package kikcode - -import ( - "errors" - "fmt" - "image/color" - "math" -) - -const ( - maxKikCodePayloadDataLength = 40 -) - -var ( - ErrEmptyData = errors.New("payload data is empty") - ErrDataTooLong = errors.New("payload data is too long") - ErrInvalidSize = errors.New("invalid size") -) - -const ( - kikCodeInnerRingRatio = 0.32 - kikCodeFirstRingRatio = 0.425 - kikCodeLastRingRatio = 0.95 - kikCodeScaleFactor = 8.0 - kikCodeRingCount = 6 -) - -type coordinate struct { - X float64 - Y float64 -} - -// todo: Use proper arc struct -type Description struct { - dimension float64 - centerPathString string - dotPathStrings []string - arcPathStrings []string - dotDimension float64 -} - -type KikCodePayload []byte - -func GenerateDescription(dimension float64, data KikCodePayload) (*Description, error) { - if dimension <= 0 { - return nil, ErrInvalidSize - } - - if len(data) == 0 { - return nil, ErrEmptyData - } else if len(data) >= maxKikCodePayloadDataLength { - return nil, ErrDataTooLong - } - - center := coordinate{ - X: 0.5 * dimension, - Y: 0.5 * dimension, - } - - outerRingWidth := dimension * 0.5 - innerRingWidth := kikCodeInnerRingRatio * outerRingWidth - firstRingWidth := kikCodeFirstRingRatio * outerRingWidth - lastRingWidth := kikCodeLastRingRatio * outerRingWidth - - centerPathString := fmt.Sprintf( - "M%[1]f,%[2]f m-%[3]f,0 a%[3]f,%[3]f 0 1,0 %[4]f,0 a%[3]f,%[3]f 0 1,0 -%[4]f,0", - center.X, - center.Y, - innerRingWidth, - 2*innerRingWidth, - ) - - offset := 0 - - ringWidth := (lastRingWidth - firstRingWidth) / kikCodeRingCount - dotSize := (ringWidth * 3.0) / 4.0 - - var dotPathStrings, arcPathStrings []string - - for ring := 0; ring < kikCodeRingCount; ring++ { - r := ringWidth*float64(ring) + firstRingWidth - if ring == 0 { - r -= innerRingWidth / 10.0 - } - - n := kikCodeScaleFactor*ring + 32 - delta := (math.Pi * 2.0) / float64(n) - - startOffset := offset - - for a := 0; a < n; a++ { - angle := float64(a)*delta - math.Pi/2.0 - - bitMask := 0x1 << (offset % 8) - byteIndex := int(math.Floor(float64(offset) / 8.0)) - currentBit := byteIndex < len(data) && (int(data[byteIndex])&bitMask) != 0 - - if currentBit { - radius := r + ringWidth/2 - arcCenter := coordinate{ - X: center.X + radius*math.Cos(angle), - Y: center.Y + radius*math.Sin(angle), - } - - nextOffset := ((offset - startOffset + 1) % n) + startOffset - nextBitMask := 0x1 << (nextOffset % 8) - nextIndex := int(math.Floor(float64(nextOffset) / 8.0)) - nextBit := nextIndex < len(data) && (int(data[nextIndex])&nextBitMask) != 0 - - if nextBit { - arcPathString := fmt.Sprintf( - "M%[1]f,%[2]f A%[3]f,%[3]f 0 0,1 %[4]f,%[5]f", - center.X+radius*math.Cos(angle), - center.Y+radius*math.Sin(angle), - radius, - center.X+radius*math.Cos(angle+delta), - center.Y+radius*math.Sin(angle+delta), - ) - arcPathStrings = append(arcPathStrings, arcPathString) - } else { - dotPathString := fmt.Sprintf( - "M%[1]f,%[2]f m-%[3]f,0 a%[3]f,%[3]f 0 1,0 %[4]f,0 a%[3]f,%[3]f 0 1,0 -%[4]f,0", - arcCenter.X, - arcCenter.Y, - 0.5*dotSize, - dotSize, - ) - dotPathStrings = append(dotPathStrings, dotPathString) - } - } - - offset += 1 - } - } - - return &Description{ - dimension: dimension, - centerPathString: centerPathString, - dotPathStrings: dotPathStrings, - arcPathStrings: arcPathStrings, - dotDimension: dotSize, - }, nil -} - -func CreateKikCodePayload(data []byte) KikCodePayload { - finderBytes := []byte{0xb2, 0xcb, 0x25, 0xc6} - return append(finderBytes, data...) -} - -type QrCodeRenderOptions struct { - ForegroundColor color.Color - - IncludeBackground bool - BackgroundColor color.Color -} - -func (d *Description) ToSvg(opts *QrCodeRenderOptions) string { - foregroundColorHex := hexColor(opts.ForegroundColor) - backgroundColorHex := hexColor(opts.BackgroundColor) - - svg := fmt.Sprintf(``, d.dimension) - if opts.IncludeBackground { - svg += fmt.Sprintf(``, d.dimension/2, backgroundColorHex) - } - svg += fmt.Sprintf(``, d.centerPathString, foregroundColorHex) - for _, arc := range d.arcPathStrings { - svg += fmt.Sprintf(``, arc, foregroundColorHex) - } - for _, dot := range d.dotPathStrings { - svg += fmt.Sprintf(``, dot, foregroundColorHex) - } - svg += `` - return svg -} - -func hexColor(c color.Color) string { - rgba := color.RGBAModel.Convert(c).(color.RGBA) - return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) -} diff --git a/pkg/kikcode/rendezvous.go b/pkg/kikcode/rendezvous.go deleted file mode 100644 index ff1bc3ee..00000000 --- a/pkg/kikcode/rendezvous.go +++ /dev/null @@ -1,11 +0,0 @@ -package kikcode - -import ( - "crypto/ed25519" - "crypto/sha256" -) - -func DeriveRendezvousPrivateKey(payload *Payload) ed25519.PrivateKey { - hashed := sha256.Sum256(payload.ToBytes()) - return ed25519.NewKeyFromSeed(hashed[:]) -} diff --git a/pkg/kin/kin.go b/pkg/kin/kin.go deleted file mode 100644 index c01881cc..00000000 --- a/pkg/kin/kin.go +++ /dev/null @@ -1,23 +0,0 @@ -package kin - -import ( - "crypto/ed25519" -) - -const ( - Mint = "kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6" - QuarksPerKin = 100000 - Decimals = 5 -) - -var ( - TokenMint = ed25519.PublicKey{11, 51, 56, 160, 171, 44, 200, 65, 213, 176, 20, 188, 106, 60, 247, 86, 41, 24, 116, 179, 25, 201, 81, 125, 155, 191, 169, 228, 233, 102, 30, 249} -) - -func FromQuarks(quarks uint64) uint64 { - return quarks / QuarksPerKin -} - -func ToQuarks(kin uint64) uint64 { - return kin * QuarksPerKin -} diff --git a/pkg/kin/memo.go b/pkg/kin/memo.go deleted file mode 100644 index 2e0cf55f..00000000 --- a/pkg/kin/memo.go +++ /dev/null @@ -1,183 +0,0 @@ -package kin - -import ( - "encoding/base64" - - "github.com/pkg/errors" -) - -// todo: formalize -const magicByte = 0x1 - -// TransactionType is the memo transaction type. -type TransactionType int16 - -const ( - TransactionTypeUnknown TransactionType = iota - 1 - TransactionTypeNone - TransactionTypeEarn - TransactionTypeSpend - TransactionTypeP2P -) - -// HighestVersion is the highest 'supported' memo version by -// the implementation. -const HighestVersion = 1 - -// MaxTransactionType is the maximum transaction type 'supported' -// by the implementation. -const MaxTransactionType = TransactionTypeP2P - -// Memo is the 32 byte memo encoded into transactions, as defined -// in github.com/kinecosystem/agora-api. -type Memo [32]byte - -// NewMemo creates a new Memo with the specified parameters. -func NewMemo(v byte, t TransactionType, appIndex uint16, foreignKey []byte) (m Memo, err error) { - if len(foreignKey) > 29 { - return m, errors.Errorf("invalid foreign key length: %d", len(foreignKey)) - } - if v > 7 { - return m, errors.Errorf("invalid version") - } - - if t < 0 || t > 31 { - return m, errors.Errorf("invalid transaction type") - } - - m[0] = magicByte - m[0] |= v << 2 - m[0] |= (byte(t) & 0x7) << 5 - - m[1] = (byte(t) & 0x18) >> 3 - m[1] |= byte(appIndex&0x3f) << 2 - - m[2] = byte((appIndex & 0x3fc0) >> 6) - - m[3] = byte((appIndex & 0xc000) >> 14) - - if len(foreignKey) > 0 { - m[3] |= (foreignKey[0] & 0x3f) << 2 - - // insert the rest of the fk. since each loop references fk[n] and fk[n+1], the upper bound is offset by 3 instead of 4. - for i := 4; i < 3+len(foreignKey); i++ { - // apply last 2-bits of current byte - // apply first 6-bits of next byte - m[i] = (foreignKey[i-4] >> 6) & 0x3 - m[i] |= (foreignKey[i-3] & 0x3f) << 2 - } - - // if the foreign key is less than 29 bytes, the last 2 bits of the FK can be included in the memo - if len(foreignKey) < 29 { - m[len(foreignKey)+3] = (foreignKey[len(foreignKey)-1] >> 6) & 0x3 - } - } - - return m, nil -} - -func MemoFromBase64String(b64 string, strict bool) (m Memo, err error) { - b, err := base64.StdEncoding.DecodeString(b64) - if err != nil { - return m, errors.Wrap(err, "invalid b64") - } - - copy(m[:], b) - if strict { - if ok := IsValidMemoStrict(m); !ok { - return m, errors.New("not a valid memo") - } - return m, nil - } - - if ok := IsValidMemo(m); !ok { - return m, errors.New("not a valid memo") - } - - return m, nil -} - -// IsValidMemo returns whether or not the memo is valid. -// -// It should be noted that there are no guarantees if the -// memo is valid, only if the memo is invalid. That is, this -// function may return false positives. -// -// Stricter validation can be done via the IsValidMemoStrict. -// However, IsValidMemoStrict is not as forward compatible as -// IsValidMemo. -func IsValidMemo(m Memo) bool { - if m[0]&0x3 != magicByte { - return false - } - - return m.TransactionTypeRaw() != TransactionTypeUnknown -} - -// IsValidMemoStrict returns whether or not the memo is valid -// checking against this SDKs supported version and transaction types. -// -// It should be noted that there are no guarantees if the -// memo is valid, only if the memo is invalid. That is, this -// function may return false positives. -func IsValidMemoStrict(m Memo) bool { - if !IsValidMemo(m) { - return false - } - - if m.Version() > HighestVersion { - return false - } - if m.TransactionType() == TransactionTypeUnknown { - return false - } - - return true -} - -// Version returns the version of the memo. -func (m Memo) Version() byte { - return (m[0] & 0x1c) >> 2 -} - -// TransactionType returns the transaction type of the memo. -func (m Memo) TransactionType() TransactionType { - if t := m.TransactionTypeRaw(); t <= MaxTransactionType { - return t - } - - return TransactionTypeUnknown -} - -// TransactionTypeRaw returns the transaction type of the memo, -// even if it is unsupported by this SDK. It should only be used -// as a fall back if the raw value is needed when TransactionType() -// yields TransactionTypeUnknown. -func (m Memo) TransactionTypeRaw() TransactionType { - // Note: we intentionally don't wrap around to Unknown - // for debugging and potential future compat. - return TransactionType((m[0] >> 5) | (m[1]&0x3)<<3) -} - -// AppIndex returns the app index of the memo. -func (m Memo) AppIndex() uint16 { - a := uint16(m[1]) >> 2 - b := uint16(m[2]) << 6 - c := uint16(m[3]) & 0x3 << 14 - return a | b | c -} - -// ForeignKey returns the foreign key of the memo. -func (m Memo) ForeignKey() (fk []byte) { - fk = make([]byte, 29) - for i := 0; i < 28; i++ { - fk[i] |= m[i+3] >> 2 - fk[i] |= m[i+4] & 0x3 << 6 - } - - // We only have 230 bits, which results in - // our last fk byte only having 6 'valid' bits - fk[28] = m[31] >> 2 - - return fk -} diff --git a/pkg/kin/memo_test.go b/pkg/kin/memo_test.go deleted file mode 100644 index c24406bc..00000000 --- a/pkg/kin/memo_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package kin - -import ( - "encoding/base64" - "math" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestMemo_Valid(t *testing.T) { - var emptyFK = make([]byte, 29) - - for v := byte(0); v <= 7; v++ { - m, err := NewMemo(v, TransactionTypeEarn, 1, make([]byte, 29)) - require.NoError(t, err) - - require.EqualValues(t, magicByte, m[0]&0x3) - require.EqualValues(t, v, m.Version()) - require.EqualValues(t, TransactionTypeEarn, m.TransactionType()) - require.EqualValues(t, 1, m.AppIndex()) - require.EqualValues(t, emptyFK, m.ForeignKey()) - } - - for txType := TransactionTypeNone; txType <= MaxTransactionType; txType++ { - m, err := NewMemo(1, txType, 1, make([]byte, 29)) - require.NoError(t, err) - - require.EqualValues(t, magicByte, m[0]&0x3) - require.EqualValues(t, 1, m.Version()) - require.EqualValues(t, txType, m.TransactionType()) - require.EqualValues(t, 1, m.AppIndex()) - require.EqualValues(t, emptyFK, m.ForeignKey()) - } - - for i := uint16(0); i < math.MaxUint16; i++ { - m, err := NewMemo(1, TransactionTypeEarn, i, make([]byte, 29)) - require.NoError(t, err) - - require.EqualValues(t, magicByte, m[0]&0x3) - require.EqualValues(t, 1, m.Version()) - require.EqualValues(t, TransactionTypeEarn, m.TransactionType()) - require.EqualValues(t, i, m.AppIndex()) - require.EqualValues(t, emptyFK, m.ForeignKey()) - } - - for i := 0; i < 256; i += 29 { - fk := make([]byte, 29) - for j := 0; j < 29; j++ { - fk[j] = byte(i + j) // this eventually overflows, but that's ok - } - - m, err := NewMemo(1, TransactionTypeEarn, 2, fk) - require.NoError(t, err) - - actual := m.ForeignKey() - for j := 0; j < 28; j++ { - require.Equal(t, fk[j], actual[j]) - } - - // Note, because we only have 230 bits, the last byte in the memo fk - // only has the first 6 bits of the last byte in the original fk. - require.Equal(t, fk[28]&0x3f, actual[28]) - } - - // Test a short foreign key - fk := []byte{byte(1), byte(255)} - m, err := NewMemo(1, TransactionTypeEarn, 2, fk) - require.NoError(t, err) - - actual := m.ForeignKey() - require.Equal(t, fk, actual[:2]) - - // Test no foreign key - m, err = NewMemo(1, TransactionTypeEarn, 2, nil) - require.NoError(t, err) - - actual = m.ForeignKey() - require.Equal(t, make([]byte, 29), actual) -} - -func TestMemo_TransactionTypeRaw(t *testing.T) { - for i := 0; i < 32; i++ { - m, err := NewMemo(1, TransactionType(i), 0, nil) - require.NoError(t, err) - require.Equal(t, m.TransactionTypeRaw(), TransactionType(i)) - } -} - -func TestMemo_TransactionType(t *testing.T) { - for txType := TransactionTypeNone; txType <= MaxTransactionType; txType++ { - m, err := NewMemo(1, txType, 0, nil) - require.NoError(t, err) - require.Equal(t, m.TransactionType(), txType) - } - - m, err := NewMemo(1, MaxTransactionType+1, 0, nil) - require.NoError(t, err) - require.Equal(t, m.TransactionType(), TransactionTypeUnknown) -} - -func TestMemo_Invalid(t *testing.T) { - // Invalid version - _, err := NewMemo(8, TransactionTypeEarn, 1, make([]byte, 29)) - require.NotNil(t, err) - - m, err := NewMemo(1, TransactionTypeEarn, 1, make([]byte, 29)) - require.Nil(t, err) - require.True(t, IsValidMemo(m)) - require.True(t, IsValidMemoStrict(m)) - - // Invalid magic byte - m[0] &= 0xfc - require.False(t, IsValidMemo(m)) - require.False(t, IsValidMemoStrict(m)) - - // Invalid transaction type - _, err = NewMemo(1, TransactionTypeUnknown, 1, make([]byte, 29)) - require.NotNil(t, err) - - m, err = NewMemo(1, MaxTransactionType+1, 1, make([]byte, 29)) - require.Nil(t, err) - require.True(t, IsValidMemo(m)) - require.False(t, IsValidMemoStrict(m)) - - // Version higher than configured - m, err = NewMemo(7, TransactionTypeEarn, 1, make([]byte, 29)) - require.Nil(t, err) - require.True(t, IsValidMemo(m)) - require.False(t, IsValidMemoStrict(m)) - - // Transaction type higher than configured - m, err = NewMemo(1, MaxTransactionType+1, 1, make([]byte, 29)) - require.Nil(t, err) - require.True(t, IsValidMemo(m)) - require.False(t, IsValidMemoStrict(m)) -} - -func TestMemoFromBase64(t *testing.T) { - validMemo, _ := NewMemo(2, TransactionTypeEarn, 1, make([]byte, 29)) - actual, err := MemoFromBase64String(base64.StdEncoding.EncodeToString(validMemo[:]), false) - require.NoError(t, err) - require.Equal(t, validMemo, actual) - - _, err = MemoFromBase64String(base64.StdEncoding.EncodeToString(validMemo[:]), true) - require.Error(t, err) - - strictlyValidMemo, _ := NewMemo(1, TransactionTypeEarn, 1, make([]byte, 29)) - actual, err = MemoFromBase64String(base64.StdEncoding.EncodeToString(strictlyValidMemo[:]), false) - require.NoError(t, err) - require.Equal(t, strictlyValidMemo, actual) - - actual, err = MemoFromBase64String(base64.StdEncoding.EncodeToString(strictlyValidMemo[:]), true) - require.NoError(t, err) - require.Equal(t, strictlyValidMemo, actual) - - invalidMemos := []string{ - "somememo", - base64.StdEncoding.EncodeToString([]byte("somememo")), - } - for _, m := range invalidMemos { - _, err := MemoFromBase64String(m, false) - require.Error(t, err) - } -} diff --git a/pkg/kin/utils.go b/pkg/kin/utils.go deleted file mode 100644 index 75ba9432..00000000 --- a/pkg/kin/utils.go +++ /dev/null @@ -1,69 +0,0 @@ -package kin - -import ( - "fmt" - "strconv" - "strings" - - "github.com/pkg/errors" -) - -// StrToQuarks converts a string representation of kin -// the quark value. -// -// An error is returned if the value string is invalid, or -// it cannot be accurately represented as quarks. For example, -// a value smaller than quarks, or a value _far_ greater than -// the supply. -func StrToQuarks(val string) (int64, error) { - parts := strings.Split(val, ".") - if len(parts) > 2 { - return 0, errors.New("invalid kin value") - } - - if len(parts[0]) > 14 { - return 0, errors.New("value cannot be represented") - } - - kin, err := strconv.ParseInt(parts[0], 10, 64) - if err != nil { - return 0, err - } - - var quarks uint64 - if len(parts) == 2 { - if len(parts[1]) > 5 { - return 0, errors.New("value cannot be represented") - } - - padded := fmt.Sprintf("%s%s", parts[1], strings.Repeat("0", 5-len(parts[1]))) - quarks, err = strconv.ParseUint(padded, 10, 64) - if err != nil { - return 0, errors.Wrap(err, "invalid decimal component") - } - } - - return kin*1e5 + int64(quarks), nil -} - -// MustStrToQuarks calls StrToQuarks, panicking if there's an error. -// -// This should only be used if you know for sure this will not panic. -func MustStrToQuarks(val string) int64 { - result, err := StrToQuarks(val) - if err != nil { - panic(err) - } - - return result -} - -// StrFromQuarks converts an int64 amount of quarks to the -// string representation of kin. -func StrFromQuarks(amount int64) string { - if amount < 1e5 { - return fmt.Sprintf("0.%05d", amount) - } - - return fmt.Sprintf("%d.%05d", amount/1e5, amount%1e5) -} diff --git a/pkg/kin/utils_test.go b/pkg/kin/utils_test.go deleted file mode 100644 index b5ed9761..00000000 --- a/pkg/kin/utils_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package kin - -import ( - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestKinToQuarks(t *testing.T) { - validCases := map[string]int64{ - "0.00001": 1, - "0.00002": 2, - "0.00020": 20, - "0.00200": 200, - "0.02000": 2000, - "0.20000": 20000, - "1.00000": 1e5, - "1.50000": 1e5 + 1e5/2, - "1": 1e5, - "2": 2e5, - // 10 trillion, which is what's in circulation - "10000000000000": 1e13 * 1e5, - // Encountered an imprecise error - "9974.99900": 997499900, - } - for in, expected := range validCases { - actual, err := StrToQuarks(in) - assert.NoError(t, err) - assert.Equal(t, expected, actual) - - if strings.Contains(in, ".") { - assert.Equal(t, in, StrFromQuarks(expected)) - } else { - assert.Equal(t, fmt.Sprintf("%s.00000", in), StrFromQuarks(expected)) - } - } - // Ensure odd padding works. - validCases = map[string]int64{ - "0.00001": 1, - "0.00002": 2, - "0.0002": 20, - "0.002": 200, - "0.02": 2000, - "0.2": 20000, - "1.000": 1e5, - "1.500": 1e5 + 1e5/2, - "1": 1e5, - "2": 2e5, - "341856.59000": 34185659000, - "341856.59": 34185659000, - "9974.99900": 997499900, - } - for in, expected := range validCases { - actual, err := StrToQuarks(in) - assert.NoError(t, err) - assert.Equal(t, expected, actual) - } - - invalidCases := []string{ - "0.000001", - "0.000015", - // 1000 trillion-1, ~100x more than what's in circulation - "999999999999999", - "abc", - "10.-1", - "10.0.0", - ".0", - } - for _, in := range invalidCases { - actual, err := StrToQuarks(in) - assert.Error(t, err) - assert.Equal(t, int64(0), actual) - } -} diff --git a/pkg/netutil/ip.go b/pkg/netutil/ip.go index c60fe5c7..bd420af1 100644 --- a/pkg/netutil/ip.go +++ b/pkg/netutil/ip.go @@ -1,31 +1,9 @@ package netutil import ( - "context" "net" - - "github.com/oschwald/maxminddb-golang" - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/pointer" ) -type IpMetadata struct { - City *string - Country *string -} - -type maxMindRecord struct { - City struct { - Names struct { - En string `maxminddb:"en"` - } `maxminddb:"names"` - } `maxminddb:"city"` - Country struct { - ISOCode string `maxminddb:"iso_code"` - } `maxminddb:"country"` -} - // GetOutboundIP gets the locally preferred outbound IP address // // From https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go @@ -40,27 +18,3 @@ func GetOutboundIP() net.IP { return localAddr.IP } - -// GetIpMetadata gets metadata about an IP. Information is provided on a best-effort -// basis. -func GetIpMetadata(ctx context.Context, db *maxminddb.Reader, ip string) (*IpMetadata, error) { - if db == nil { - return &IpMetadata{}, nil - } - - parsed := net.ParseIP(ip) - if parsed == nil { - return nil, errors.New("cannot parse ip") - } - - var metadata maxMindRecord - err := db.Lookup(parsed, &metadata) - if err != nil { - return nil, errors.Wrap(err, "error looking up ip metadata") - } - - return &IpMetadata{ - City: pointer.StringIfValid(len(metadata.City.Names.En) > 0, metadata.City.Names.En), - Country: pointer.StringIfValid(len(metadata.Country.ISOCode) > 0, metadata.Country.ISOCode), - }, nil -} diff --git a/pkg/phone/mcc.go b/pkg/phone/mcc.go deleted file mode 100644 index 0dd3ba0e..00000000 --- a/pkg/phone/mcc.go +++ /dev/null @@ -1,268 +0,0 @@ -package phone - -import "strings" - -var MccToIso = map[int]map[string]struct{}{ - 202: {"GR": {}}, - 204: {"NL": {}}, - 206: {"BE": {}}, - 208: {"FR": {}}, - 212: {"MC": {}}, - 213: {"AD": {}}, - 214: {"ES": {}}, - 216: {"HU": {}}, - 218: {"BA": {}}, - 219: {"HR": {}}, - 220: {"RS": {}}, - 221: {"XK": {}}, - 222: {"IT": {}}, - 226: {"RO": {}}, - 228: {"CH": {}}, - 230: {"CZ": {}}, - 231: {"SK": {}}, - 232: {"AT": {}}, - 234: {"GG": {}, "IM": {}, "JE": {}, "GB": {}}, - 235: {"GB": {}}, - 238: {"DK": {}}, - 240: {"SE": {}}, - 242: {"NO": {}}, - 244: {"FI": {}}, - 246: {"LT": {}}, - 247: {"LV": {}}, - 248: {"EE": {}}, - 250: {"RU": {}}, - 255: {"UA": {}}, - 257: {"BY": {}}, - 259: {"MD": {}}, - 260: {"PL": {}}, - 262: {"DE": {}}, - 266: {"GI": {}}, - 268: {"PT": {}}, - 270: {"LU": {}}, - 272: {"IE": {}}, - 274: {"IS": {}}, - 276: {"AL": {}}, - 278: {"MT": {}}, - 280: {"CY": {}}, - 282: {"GE": {}}, - 283: {"AM": {}}, - 284: {"BG": {}}, - 286: {"TR": {}}, - 288: {"FO": {}}, - 289: {"GE": {}}, - 290: {"GL": {}}, - 292: {"SM": {}}, - 293: {"SI": {}}, - 294: {"MK": {}}, - 295: {"LI": {}}, - 297: {"ME": {}}, - 302: {"CA": {}}, - 308: {"PM": {}}, - 310: {"GU": {}, "MP": {}, "US": {}}, - 311: {"GU": {}, "US": {}}, - 312: {"US": {}}, - 313: {"US": {}}, - 314: {"US": {}}, - 315: {"US": {}}, - 316: {"US": {}}, - 330: {"PR": {}}, - 332: {"VI": {}}, - 334: {"MX": {}}, - 338: {"JM": {}}, - 340: {"BL": {}, "GP": {}, "MF": {}, "MQ": {}}, - 342: {"BB": {}}, - 344: {"AG": {}}, - 346: {"KY": {}}, - 348: {"VG": {}}, - 350: {"BM": {}}, - 352: {"GD": {}}, - 354: {"MS": {}}, - 356: {"KN": {}}, - 358: {"LC": {}}, - 360: {"VC": {}}, - 362: {"BQ": {}, "CW": {}, "SX": {}}, - 363: {"AW": {}}, - 364: {"BS": {}}, - 365: {"AI": {}}, - 366: {"DM": {}}, - 368: {"CU": {}}, - 370: {"DO": {}}, - 372: {"HT": {}}, - 374: {"TT": {}}, - 376: {"TC": {}}, - 400: {"AZ": {}}, - 401: {"KZ": {}}, - 402: {"BT": {}}, - 404: {"IN": {}}, - 405: {"IN": {}}, - 406: {"IN": {}}, - 410: {"PK": {}}, - 412: {"AF": {}}, - 413: {"LK": {}}, - 414: {"MM": {}}, - 415: {"LB": {}}, - 416: {"JO": {}}, - 417: {"SY": {}}, - 418: {"IQ": {}}, - 419: {"KW": {}}, - 420: {"SA": {}}, - 421: {"YE": {}}, - 422: {"OM": {}}, - 424: {"AE": {}}, - 425: {"IL": {}, "PS": {}}, - 426: {"BH": {}}, - 427: {"QA": {}}, - 428: {"MN": {}}, - 429: {"NP": {}}, - 430: {"AE": {}}, - 431: {"AE": {}}, - 432: {"IR": {}}, - 434: {"UZ": {}}, - 436: {"TJ": {}}, - 437: {"KG": {}}, - 438: {"TM": {}}, - 440: {"JP": {}}, - 441: {"JP": {}}, - 450: {"KR": {}}, - 452: {"VN": {}}, - 454: {"HK": {}}, - 455: {"MO": {}}, - 456: {"KH": {}}, - 457: {"LA": {}}, - 460: {"CN": {}}, - 461: {"CN": {}}, - 466: {"TW": {}}, - 467: {"KP": {}}, - 470: {"BD": {}}, - 472: {"MV": {}}, - 502: {"MY": {}}, - 505: {"AU": {}, "NF": {}}, - 510: {"ID": {}}, - 514: {"TL": {}}, - 515: {"PH": {}}, - 520: {"TH": {}}, - 525: {"SG": {}}, - 528: {"BN": {}}, - 530: {"NZ": {}}, - 536: {"NR": {}}, - 537: {"PG": {}}, - 539: {"TO": {}}, - 540: {"SB": {}}, - 541: {"VU": {}}, - 542: {"FJ": {}}, - 543: {"WF": {}}, - 544: {"AS": {}}, - 545: {"KI": {}}, - 546: {"NC": {}}, - 547: {"PF": {}}, - 548: {"CK": {}}, - 549: {"WS": {}}, - 550: {"FM": {}}, - 551: {"MH": {}}, - 552: {"PW": {}}, - 553: {"TV": {}}, - 554: {"TK": {}}, - 555: {"NU": {}}, - 602: {"EG": {}}, - 603: {"DZ": {}}, - 604: {"MA": {}}, - 605: {"TN": {}}, - 606: {"LY": {}}, - 607: {"GM": {}}, - 608: {"SN": {}}, - 609: {"MR": {}}, - 610: {"ML": {}}, - 611: {"GN": {}}, - 612: {"CI": {}}, - 613: {"BF": {}}, - 614: {"NE": {}}, - 615: {"TG": {}}, - 616: {"BJ": {}}, - 617: {"MU": {}}, - 618: {"LR": {}}, - 619: {"SL": {}}, - 620: {"GH": {}}, - 621: {"NG": {}}, - 622: {"TD": {}}, - 623: {"CF": {}}, - 624: {"CM": {}}, - 625: {"CV": {}}, - 626: {"ST": {}}, - 627: {"GQ": {}}, - 628: {"GA": {}}, - 629: {"CG": {}}, - 630: {"CD": {}}, - 631: {"AO": {}}, - 632: {"GW": {}}, - 633: {"SC": {}}, - 634: {"SD": {}}, - 635: {"RW": {}}, - 636: {"ET": {}}, - 637: {"SO": {}}, - 638: {"DJ": {}}, - 639: {"KE": {}}, - 640: {"TZ": {}}, - 641: {"UG": {}}, - 642: {"BI": {}}, - 643: {"MZ": {}}, - 645: {"ZM": {}}, - 646: {"MG": {}}, - 647: {"RE": {}, "YT": {}}, - 648: {"ZW": {}}, - 649: {"NA": {}}, - 650: {"MW": {}}, - 651: {"LS": {}}, - 652: {"BW": {}}, - 653: {"SZ": {}}, - 654: {"KM": {}}, - 655: {"ZA": {}}, - 657: {"ER": {}}, - 658: {"SH": {}}, - 659: {"SS": {}}, - 702: {"BZ": {}}, - 704: {"GT": {}}, - 706: {"SV": {}}, - 708: {"HN": {}}, - 710: {"NI": {}}, - 712: {"CR": {}}, - 714: {"PA": {}}, - 716: {"PE": {}}, - 722: {"AR": {}}, - 724: {"BR": {}}, - 730: {"CL": {}}, - 732: {"CO": {}}, - 734: {"VE": {}}, - 736: {"BO": {}}, - 738: {"GY": {}}, - 740: {"EC": {}}, - 742: {"GF": {}}, - 744: {"PY": {}}, - 746: {"SR": {}}, - 748: {"UY": {}}, - 750: {"FK": {}}, - 995: {"IO": {}}, -} - -var IsoToMcc map[string]map[int]struct{} - -func init() { - IsoToMcc = make(map[string]map[int]struct{}) - for mcc, isoCodes := range MccToIso { - for isoCode := range isoCodes { - if _, ok := IsoToMcc[isoCode]; !ok { - IsoToMcc[isoCode] = make(map[int]struct{}) - } - IsoToMcc[isoCode][mcc] = struct{}{} - } - } -} - -// IsMccIsoCode determines whether a MCC is associated with the provided ISO country code -func IsMccIsoCode(mcc int, isoCode string) bool { - isoCodes, ok := MccToIso[mcc] - if !ok { - return false - } - _, ok = isoCodes[strings.ToUpper(isoCode)] - return ok -} diff --git a/pkg/phone/memory/verifier.go b/pkg/phone/memory/verifier.go deleted file mode 100644 index 5a46fc7b..00000000 --- a/pkg/phone/memory/verifier.go +++ /dev/null @@ -1,128 +0,0 @@ -package mock - -import ( - "context" - "sync" - - "github.com/google/uuid" - - "github.com/code-payments/code-server/pkg/phone" -) - -const ( - // ValidPhoneVerificationToken can be used to test successful confirmation - // of active phone verifications. - ValidPhoneVerificationToken = "123456" - - // InvalidPhoneVerificationToken can be used to test unsuccessful confirmation - // of phone verifications. - InvalidPhoneVerificationToken = "999999" -) - -type verifier struct { - mu sync.Mutex - - activeVerificationsByID map[string]string - activeVerificationsByNumber map[string]string -} - -// NewVerifier returns a new in memory phone verifier that always "sends" a -// code with value 123456. Verifications are long lived and will be completed -// when either canceled or pass validation. -func NewVerifier() phone.Verifier { - return &verifier{ - activeVerificationsByID: make(map[string]string), - activeVerificationsByNumber: make(map[string]string), - } -} - -// SendCode implements phone.Verifier.SendCode -func (v *verifier) SendCode(ctx context.Context, phoneNumber string) (string, *phone.Metadata, error) { - if !phone.IsE164Format(phoneNumber) { - return "", nil, phone.ErrInvalidNumber - } - - v.mu.Lock() - defer v.mu.Unlock() - - metadata := &phone.Metadata{ - PhoneNumber: phoneNumber, - } - metadata.SetType(phone.TypeMobile) - metadata.SetMobileCountryCode(302) // Canada - metadata.SetMobileNetworkCode(720) // Rogers - - // There's already an active verification, so simulate re-sending the code - if id, ok := v.activeVerificationsByNumber[phoneNumber]; ok { - return id, metadata, nil - } - - // Otherwise, create a new verification and simulate sending the code for the - // first time - id := uuid.New().String() - v.activeVerificationsByID[id] = phoneNumber - v.activeVerificationsByNumber[phoneNumber] = id - - return id, metadata, nil -} - -// Check implements phone.Verifier.Check -func (v *verifier) Check(ctx context.Context, phoneNumber, code string) error { - if !phone.IsVerificationCode(code) { - return phone.ErrInvalidVerificationCode - } - - v.mu.Lock() - defer v.mu.Unlock() - - id, ok := v.activeVerificationsByNumber[phoneNumber] - - // There's no active verifications - if !ok { - return phone.ErrNoVerification - } - - // There's an active verification, but the code doesn't match - if code != ValidPhoneVerificationToken { - return phone.ErrInvalidVerificationCode - } - - // The code matches and the verification is complete - delete(v.activeVerificationsByID, id) - delete(v.activeVerificationsByNumber, phoneNumber) - - return nil -} - -// Cancel implements phone.Verifier.Cancel -func (v *verifier) Cancel(ctx context.Context, id string) error { - v.mu.Lock() - defer v.mu.Unlock() - - phoneNumber, ok := v.activeVerificationsByID[id] - - // There's no active verification for the phone number - if !ok { - return nil - } - - // Simulate canceling the verification by removing the verification. - delete(v.activeVerificationsByID, id) - delete(v.activeVerificationsByNumber, phoneNumber) - - return nil -} - -// IsVerificationActive implements phone.Verifier.IsVerificationActive -func (v *verifier) IsVerificationActive(ctx context.Context, id string) (bool, error) { - v.mu.Lock() - defer v.mu.Unlock() - - _, ok := v.activeVerificationsByID[id] - return ok, nil -} - -// IsValidPhoneNumber implements phone.Verifier.IsValidPhoneNumber -func (v *verifier) IsValidPhoneNumber(_ context.Context, phoneNumber string) (bool, error) { - return phone.IsE164Format(phoneNumber), nil -} diff --git a/pkg/phone/metadata.go b/pkg/phone/metadata.go deleted file mode 100644 index 4d5d2002..00000000 --- a/pkg/phone/metadata.go +++ /dev/null @@ -1,39 +0,0 @@ -package phone - -// Identifies type of phone -type Type uint8 - -const ( - TypeUnknown Type = iota - TypeMobile - TypeVoip - TypeLandline -) - -// Metadata provides additional information regarding a phone number. Information -// is provided on a best-effort basis as provided by third party solutions. This -// can be used for antispam and fraud measures. -type Metadata struct { - // The phone number associated with the set of metadata - PhoneNumber string - - // The type of phone. Currently, this is always expected to be a mobile type. - Type *Type - - // Identifies the country and MNO - // https://www.twilio.com/docs/iot/supersim/api/network-resource#the-identifiers-property - MobileCountryCode *int - MobileNetworkCode *int -} - -func (m *Metadata) SetType(t Type) { - m.Type = &t -} - -func (m *Metadata) SetMobileCountryCode(mcc int) { - m.MobileCountryCode = &mcc -} - -func (m *Metadata) SetMobileNetworkCode(mnc int) { - m.MobileNetworkCode = &mnc -} diff --git a/pkg/phone/twilio/verifier.go b/pkg/phone/twilio/verifier.go deleted file mode 100644 index 1fd112bc..00000000 --- a/pkg/phone/twilio/verifier.go +++ /dev/null @@ -1,431 +0,0 @@ -package twilio - -import ( - "context" - "net/http" - "strconv" - "strings" - - "github.com/pkg/errors" - - "github.com/twilio/twilio-go" - "github.com/twilio/twilio-go/client" - lookupsv1 "github.com/twilio/twilio-go/rest/lookups/v1" - verifyv2 "github.com/twilio/twilio-go/rest/verify/v2" - - grpc_client "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/phone" -) - -const ( - metricsStructName = "phone.twilio.verifier" -) - -var ( - androidAppHash = "+8j1B159Xfs" -) - -const ( - // https://www.twilio.com/docs/api/errors/60200 - invalidParameterCode = 60200 - // https://www.twilio.com/docs/api/errors/60202 - maxCheckAttemptsCode = 60202 - // https://www.twilio.com/docs/api/errors/60203 - maxSendAttemptsCode = 60203 - // https://www.twilio.com/docs/api/errors/60220 - useCaseVettingCode = 60220 - // https://www.twilio.com/docs/api/errors/60410 - fraudDetectionCode = 60410 -) - -var ( - defaultChannel = "sms" -) - -var ( - carrierMapKey = "carrier" - mobileCountryCodeMapKey = "mobile_country_code" - mobileNetworkCodeMapKey = "mobile_network_code" - phoneTypeMapKey = "type" -) - -var ( - statusApproved = "approved" - statusCanceled = "canceled" - statusPending = "pending" -) - -// https://www.twilio.com/docs/lookup/api#phone-number-type-values -var ( - phoneTypeLandline = "landline" - phoneTypeMobile = "mobile" - phoneTypeVoip = "voip" -) - -type verifier struct { - client *twilio.RestClient - serviceSid string -} - -// NewVerifier returns a new phone verifier backed by Twilio -func NewVerifier(accountSid, serviceSid, authToken string) phone.Verifier { - client := twilio.NewRestClientWithParams(twilio.ClientParams{ - Username: accountSid, - Password: authToken, - }) - - return &verifier{ - client: client, - serviceSid: serviceSid, - } -} - -// SendCode implements phone.Verifier.SendCode -func (v *verifier) SendCode(ctx context.Context, phoneNumber string) (string, *phone.Metadata, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "SendCode") - defer tracer.End() - - err := v.checkValidPhoneNumber(phoneNumber) - if err != nil { - tracer.OnError(err) - return "", nil, err - } - - var appHash *string - userAgent, err := grpc_client.GetUserAgent(ctx) - if err == nil && userAgent.DeviceType == grpc_client.DeviceTypeAndroid { - appHash = &androidAppHash - } - - resp, err := v.client.VerifyV2.CreateVerification(v.serviceSid, &verifyv2.CreateVerificationParams{ - To: &phoneNumber, - Channel: &defaultChannel, - AppHash: appHash, - }) - if err != nil { - err = checkInvalidToParameterError(err, phone.ErrInvalidNumber) - err = checkMaxSendAttemptsError(err, phone.ErrRateLimited) - err = checkFraudDetectionError(err, phone.ErrRateLimited) - err = checkUseCaseVettingError(err, phone.ErrRateLimited) - tracer.OnError(err) - return "", nil, err - } - - if resp.Sid == nil { - err = errors.New("sid not provided") - tracer.OnError(err) - return "", nil, err - } - - metadata := getMetadataFromLookupMap(phoneNumber, resp.Lookup) - - return *resp.Sid, metadata, nil -} - -// Check implements phone.Verifier.Check -func (v *verifier) Check(ctx context.Context, phoneNumber, code string) error { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "Check") - defer tracer.End() - - if !phone.IsVerificationCode(code) { - err := phone.ErrInvalidVerificationCode - tracer.OnError(err) - return err - } - - resp, err := v.client.VerifyV2.CreateVerificationCheck(v.serviceSid, &verifyv2.CreateVerificationCheckParams{ - To: &phoneNumber, - Code: &code, - }) - if err != nil { - err = check404Error(err, phone.ErrNoVerification) - err = checkMaxCheckAttemptsError(err, phone.ErrNoVerification) - tracer.OnError(err) - return err - } - - if resp.Status == nil { - err = errors.New("status not provided") - tracer.OnError(err) - return err - } - - switch strings.ToLower(*resp.Status) { - case statusApproved: - err = nil - case statusCanceled: - err = phone.ErrNoVerification - default: - err = phone.ErrInvalidVerificationCode - } - tracer.OnError(err) - return err -} - -// Cancel implements phone.Verifier.Cancel -func (v *verifier) Cancel(ctx context.Context, id string) error { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "Cancel") - defer tracer.End() - - _, err := v.client.VerifyV2.UpdateVerification(v.serviceSid, id, &verifyv2.UpdateVerificationParams{ - Status: &statusCanceled, - }) - - if err != nil { - err := check404Error(err, nil) - tracer.OnError(err) - return err - } - - return nil -} - -// IsVerificationActive implements phone.Verifier.IsVerificationActive -func (v *verifier) IsVerificationActive(ctx context.Context, id string) (bool, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "IsVerificationActive") - defer tracer.End() - - resp, err := v.client.VerifyV2.FetchVerification(v.serviceSid, id) - if is404Error(err) { - return false, nil - } else if err != nil { - tracer.OnError(err) - return false, err - } - - if resp.Status == nil { - err = errors.New("status is not provided") - tracer.OnError(err) - return false, err - } - - return *resp.Status == statusPending, nil -} - -// IsValidPhoneNumber implements phone.Verifier.IsValidPhoneNumber -func (v *verifier) IsValidPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "IsValidPhoneNumber") - defer tracer.End() - - err := v.checkValidPhoneNumber(phoneNumber) - if err == phone.ErrInvalidNumber || err == phone.ErrUnsupportedPhoneType { - return false, nil - } else if err != nil { - tracer.OnError(err) - return false, err - } - return true, nil -} - -// todo: Use the new V2 API when we get access. It has some cool features like -// SIM swap detection. -func (v *verifier) checkValidPhoneNumber(phoneNumber string) error { - if !phone.IsE164Format(phoneNumber) { - return errors.New("phone number is not in E.164 format") - } - - resp, err := v.client.LookupsV1.FetchPhoneNumber(phoneNumber, &lookupsv1.FetchPhoneNumberParams{ - Type: &[]string{carrierMapKey}, - }) - if err != nil { - return check404Error(err, phone.ErrInvalidNumber) - } - - if resp.Carrier == nil { - return nil - } - - carrierInfoMap, ok := (*resp.Carrier).(map[string]interface{}) - if !ok { - return nil - } - metadata := getMetadataFromCarrierInfoMap(phoneNumber, carrierInfoMap) - - // This is not guaranteed to be available - if metadata.Type == nil { - return nil - } - - // todo: It's safest to block all virtual numbers, but confirm with Twilio - // if there are cases where we'd want this to go through. - if *metadata.Type != phone.TypeMobile { - return phone.ErrUnsupportedPhoneType - } - return nil -} - -func check404Error(inError, outError error) error { - if is404Error(inError) { - return outError - } - return inError -} - -func is404Error(err error) bool { - twilioError, ok := err.(*client.TwilioRestError) - if !ok { - return false - } - - return twilioError.Status == http.StatusNotFound -} - -func checkMaxSendAttemptsError(inError, outError error) error { - twilioError, ok := inError.(*client.TwilioRestError) - if !ok { - return inError - } - - if twilioError.Status != http.StatusTooManyRequests { - return inError - } - - if twilioError.Code == maxSendAttemptsCode { - return outError - } - return inError -} - -func checkMaxCheckAttemptsError(inError, outError error) error { - twilioError, ok := inError.(*client.TwilioRestError) - if !ok { - return inError - } - - if twilioError.Status != http.StatusTooManyRequests { - return inError - } - - if twilioError.Code == maxCheckAttemptsCode { - return outError - } - return inError -} - -func checkInvalidToParameterError(inError, outError error) error { - twilioError, ok := inError.(*client.TwilioRestError) - if !ok { - return inError - } - - if twilioError.Status != http.StatusBadRequest { - return inError - } - - if twilioError.Code != invalidParameterCode { - return inError - } - - expectedMessage := strings.ToLower("Invalid parameter `To`") - if strings.Contains(strings.ToLower(twilioError.Message), expectedMessage) { - return outError - } - return inError -} - -func checkUseCaseVettingError(inError, outError error) error { - twilioError, ok := inError.(*client.TwilioRestError) - if !ok { - return inError - } - - if twilioError.Code == useCaseVettingCode { - return outError - } - return inError -} - -func checkFraudDetectionError(inError, outError error) error { - twilioError, ok := inError.(*client.TwilioRestError) - if !ok { - return inError - } - - if twilioError.Code == fraudDetectionCode { - return outError - } - return inError -} - -// I'm soo sorry about the below code. The Twilio client is ridiculous with typing. - -// Note: Can't use "ok" variable because it appears to actually set a key with a nil -// value - -func getMetadataFromLookupMap(phoneNumber string, lookup *interface{}) *phone.Metadata { - metadata := &phone.Metadata{ - PhoneNumber: phoneNumber, - } - - if lookup == nil { - return metadata - } - - carrierInfo := (*lookup).(map[string]interface{})[carrierMapKey] - if carrierInfo == nil { - return metadata - } - - carrierInfoMap, ok := carrierInfo.(map[string]interface{}) - if !ok { - return metadata - } - - return getMetadataFromCarrierInfoMap(phoneNumber, carrierInfoMap) -} - -func getMetadataFromCarrierInfoMap(phoneNumber string, carrierInfo map[string]interface{}) *phone.Metadata { - metadata := &phone.Metadata{ - PhoneNumber: phoneNumber, - } - - if carrierInfo == nil { - return metadata - } - - phoneType := carrierInfo[phoneTypeMapKey] - if phoneType != nil { - strValue, ok := phoneType.(string) - if ok { - metadata.SetType(getPhoneTypeFromString(strValue)) - } - } - - mobileCountryCode := carrierInfo[mobileCountryCodeMapKey] - if mobileCountryCode != nil { - strValue, ok := mobileCountryCode.(string) - if ok { - intValue, err := strconv.Atoi(strValue) - if err == nil { - metadata.SetMobileCountryCode(intValue) - } - } - } - - mobileNetworkCode := carrierInfo[mobileNetworkCodeMapKey] - if mobileNetworkCode != nil { - strValue, ok := mobileNetworkCode.(string) - if ok { - intValue, err := strconv.Atoi(strValue) - if err == nil { - metadata.SetMobileNetworkCode(intValue) - } - } - } - - return metadata -} - -func getPhoneTypeFromString(value string) phone.Type { - switch value { - case phoneTypeMobile: - return phone.TypeMobile - case phoneTypeVoip: - return phone.TypeVoip - case phoneTypeLandline: - return phone.TypeLandline - default: - return phone.TypeUnknown - } -} diff --git a/pkg/phone/validation.go b/pkg/phone/validation.go deleted file mode 100644 index d34b50ae..00000000 --- a/pkg/phone/validation.go +++ /dev/null @@ -1,22 +0,0 @@ -package phone - -import "regexp" - -var ( - // E.164 phone number format regex provided by Twilio: https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 - phonePattern = regexp.MustCompile("^\\+[1-9]\\d{1,14}$") - - // A verification code must be a 4-10 digit string - verificationCodePattern = regexp.MustCompile("^[0-9]{4,10}$") -) - -// IsE164Format returns whether a string is a E.164 formatted phone number. -func IsE164Format(phoneNumber string) bool { - return phonePattern.Match([]byte(phoneNumber)) -} - -// IsVerificationCode returns whether a string is a 4-10 digit numberical -// verification code. -func IsVerificationCode(code string) bool { - return verificationCodePattern.Match([]byte(code)) -} diff --git a/pkg/phone/verifier.go b/pkg/phone/verifier.go deleted file mode 100644 index 2146f74e..00000000 --- a/pkg/phone/verifier.go +++ /dev/null @@ -1,51 +0,0 @@ -package phone - -import ( - "context" - "errors" -) - -var ( - // ErrInvalidNumber is returned if the phone number is invalid - ErrInvalidNumber = errors.New("phone number is invalid") - - // ErrRateLimited indicates that the call was rate limited - ErrRateLimited = errors.New("rate limited") - - // ErrInvalidVerificationCode is returned when the verification does not - // match what's expected in a Confirm call - ErrInvalidVerificationCode = errors.New("verification code does not match") - - // ErrNoVerification indicates that no verification is in progress for - // the provided phone number in a Confirm call. Several reasons this - // can occur include a verification being expired or having reached a - // maximum check threshold. - ErrNoVerification = errors.New("verification not in progress") - - // ErrUnsupportedPhoneType indicates the provided phone number maps to - // a type of phone that isn't supported. - ErrUnsupportedPhoneType = errors.New("unsupported phone type") -) - -type Verifier interface { - // SendCode sends a verification code via SMS to the provided phone number. - // If an active verification is already taking place, the existing code will - // be resent. A unique ID for the verification and phone metadata is returned on - // success. - SendCode(ctx context.Context, phoneNumber string) (string, *Metadata, error) - - // Check verifies a SMS code sent to a phone number. - Check(ctx context.Context, phoneNumber, code string) error - - // Cancel cancels an active verification. No error is returned if the - // verification doesn't exist, previously canceled or successfully - // completed. - Cancel(ctx context.Context, id string) error - - // IsVerificationActive checks whether a verification is active or not - IsVerificationActive(ctx context.Context, id string) (bool, error) - - // IsValid validates a phone number is real and is able to receive - // a verification code. - IsValidPhoneNumber(ctx context.Context, phoneNumber string) (bool, error) -} diff --git a/pkg/push/fcm/provider.go b/pkg/push/fcm/provider.go deleted file mode 100644 index d7180b4e..00000000 --- a/pkg/push/fcm/provider.go +++ /dev/null @@ -1,163 +0,0 @@ -package fcm - -import ( - "context" - "time" - - firebase "firebase.google.com/go/v4" - "firebase.google.com/go/v4/messaging" - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/metrics" - "github.com/code-payments/code-server/pkg/push" - "github.com/code-payments/code-server/pkg/retry" - "github.com/code-payments/code-server/pkg/retry/backoff" -) - -const ( - metricsStructName = "push.fcm.provider" -) - -type provider struct { - client *messaging.Client -} - -// NewPushProvider returns a new push.Provider backed by FCM -// -// Requires GOOGLE_APPLICATION_CREDENTIALS to be set: -// https://firebase.google.com/docs/admin/setup#initialize-sdk -func NewPushProvider() (push.Provider, error) { - firebaseApp, err := firebase.NewApp(context.Background(), nil) - if err != nil { - return nil, errors.Wrap(err, "error initializing firebase app") - } - - messagingClient, err := firebaseApp.Messaging(context.Background()) - if err != nil { - return nil, errors.Wrap(err, "error initializing firebase messaging client") - } - - return &provider{ - client: messagingClient, - }, nil -} - -// IsValidPushToken implements push.Provider.IsValidPushToken -func (p *provider) IsValidPushToken(ctx context.Context, pushToken string) (bool, error) { - defer metrics.TraceMethodCall(ctx, metricsStructName, "IsValidPushToken").End() - - var result bool - err := retrier(func() error { - _, err := p.client.SendDryRun(ctx, &messaging.Message{ - Token: pushToken, - Notification: &messaging.Notification{ - Title: "test", - }, - }) - - // https://firebase.google.com/docs/cloud-messaging/manage-tokens#detect-invalid-token-responses-from-the-fcm-backend - if messaging.IsInvalidArgument(err) || messaging.IsUnregistered(err) || messaging.IsSenderIDMismatch(err) { - return nil - } else if err != nil { - return err - } - - result = true - return nil - }) - - if err != nil { - return false, errors.Wrap(err, "error sending dry run message") - } - return result, nil -} - -// SendPush implements push.Provider.SendPush -func (p *provider) SendPush(ctx context.Context, pushToken, title, body string) error { - defer metrics.TraceMethodCall(ctx, metricsStructName, "SendPush").End() - - return retrier(func() error { - _, err := p.client.Send(ctx, &messaging.Message{ - Token: pushToken, - Notification: &messaging.Notification{ - Title: title, - Body: body, - }, - }) - return err - }) -} - -// SendMutableAPNSPush implements push.Provider.SendMutableAPNSPush -func (p *provider) SendMutableAPNSPush( - ctx context.Context, - pushToken, - titleKey, category, threadId string, - kvs map[string]string, -) error { - defer metrics.TraceMethodCall(ctx, metricsStructName, "SendMutableAPNSPush").End() - - _, err := p.client.Send(ctx, &messaging.Message{ - Token: pushToken, - Data: kvs, - APNS: &messaging.APNSConfig{ - Payload: &messaging.APNSPayload{ - Aps: &messaging.Aps{ - Alert: &messaging.ApsAlert{ - TitleLocKey: titleKey, - Body: "...", - }, - Category: category, - ThreadID: threadId, - MutableContent: true, - }, - }, - }, - }) - return err -} - -// SendDataPush implements push.Provider.SendDataPush -func (p *provider) SendDataPush(ctx context.Context, pushToken string, kvs map[string]string) error { - defer metrics.TraceMethodCall(ctx, metricsStructName, "SendDataPush").End() - - return retrier(func() error { - _, err := p.client.Send(ctx, &messaging.Message{ - Token: pushToken, - Data: kvs, - }) - return err - }) -} - -// SetAPNSBadgeCount implements push.Provider.SetAPNSBadgeCount -func (p *provider) SetAPNSBadgeCount(ctx context.Context, pushToken string, count int) error { - defer metrics.TraceMethodCall(ctx, metricsStructName, "SetAPNSBadgeCount").End() - - return retrier(func() error { - _, err := p.client.Send(ctx, &messaging.Message{ - Token: pushToken, - APNS: &messaging.APNSConfig{ - Payload: &messaging.APNSPayload{ - Aps: &messaging.Aps{ - Badge: &count, - }, - }, - }, - }) - return err - }) -} - -// retrier is a common retry strategy for FCM calls -func retrier(action retry.Action) error { - _, err := retry.Retry( - action, - retry.Limit(3), - func(attempts uint, err error) bool { - return messaging.IsUnavailable(err) || messaging.IsInternal(err) - }, - retry.Backoff(backoff.BinaryExponential(250*time.Millisecond), time.Second), - ) - return err -} diff --git a/pkg/push/memory/provider.go b/pkg/push/memory/provider.go deleted file mode 100644 index 54210dd1..00000000 --- a/pkg/push/memory/provider.go +++ /dev/null @@ -1,63 +0,0 @@ -package memory - -import ( - "context" - "errors" - - "github.com/code-payments/code-server/pkg/push" -) - -const ( - // This values will pass IsValidPushToken - ValidAndroidPushToken = "test_android_push_token" - ValidApplePushToken = "test_apns_push_token" - - // This value will fail IsValidPushToken - InvalidPushToken = "invalid" -) - -type provider struct { -} - -// NewPushProvider returns a new in memory push.Provider -func NewPushProvider() push.Provider { - return &provider{} -} - -// IsValidPushToken implements push.Provider.IsValidPushToken -func (p *provider) IsValidPushToken(_ context.Context, pushToken string) (bool, error) { - return pushToken == ValidAndroidPushToken || pushToken == ValidApplePushToken, nil -} - -// SendPush implements push.Provider.SendPush -func (p *provider) SendPush(ctx context.Context, pushToken, title, body string) error { - return simulateSendingPush(pushToken) -} - -// SendMutableAPNSPush implements push.Provider.SendMutableAPNSPush -func (p *provider) SendMutableAPNSPush( - ctx context.Context, - pushToken, - titleKey, category, threadId string, - kvs map[string]string, -) error { - return simulateSendingPush(pushToken) -} - -// SendDataPush implements push.Provider.SendDataPush -func (p *provider) SendDataPush(ctx context.Context, pushToken string, kvs map[string]string) error { - return simulateSendingPush(pushToken) -} - -// SetAPNSBadgeCount implements push.Provider.SetAPNSBadgeCount -func (p *provider) SetAPNSBadgeCount(ctx context.Context, pushToken string, count int) error { - return simulateSendingPush(pushToken) -} - -func simulateSendingPush(pushToken string) error { - if pushToken == ValidAndroidPushToken || pushToken == ValidApplePushToken { - return nil - } - - return errors.New("push token is invalid") -} diff --git a/pkg/push/provider.go b/pkg/push/provider.go deleted file mode 100644 index cc98ad4c..00000000 --- a/pkg/push/provider.go +++ /dev/null @@ -1,28 +0,0 @@ -package push - -import ( - "context" -) - -type Provider interface { - // IsValidPushToken validates whether a push token is valid - IsValidPushToken(ctx context.Context, pushToken string) (bool, error) - - // SendPush sends a basic push notication with a title and body - SendPush(ctx context.Context, pushToken, title, body string) error - - // SendMutableAPNSPush sends a push over APNS with a text body that's mutable - // on the client using custom key value pairs - SendMutableAPNSPush( - ctx context.Context, - pushToken, - titleKey, category, threadId string, - kvs map[string]string, - ) error - - // SendDataPush sends a data push - SendDataPush(ctx context.Context, pushToken string, kvs map[string]string) error - - // SetAPNSBadgeCount sets the badge count on the iOS app icon - SetAPNSBadgeCount(ctx context.Context, pushToken string, count int) error -} diff --git a/pkg/twitter/client.go b/pkg/twitter/client.go deleted file mode 100644 index 6949d2ec..00000000 --- a/pkg/twitter/client.go +++ /dev/null @@ -1,405 +0,0 @@ -package twitter - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "sync" - "time" - - "github.com/dghubble/oauth1" - "github.com/pkg/errors" - - "github.com/code-payments/code-server/pkg/metrics" -) - -const ( - baseUrl = "https://api.twitter.com/2/" - - bearerTokenMaxAge = 15 * time.Minute - - metricsStructName = "twitter.client" -) - -type Client struct { - httpClient *http.Client - - clientId string - clientSecret string - accessToken string - accessTokenSecret string - - bearerTokenMu sync.RWMutex - bearerToken string - lastBearerTokenRefresh time.Time -} - -// NewClient returns a new Twitter client -func NewClient(clientId, clientSecret, accessToken, accessTokenSecret string) *Client { - return &Client{ - httpClient: http.DefaultClient, - clientId: clientId, - clientSecret: clientSecret, - accessToken: accessToken, - accessTokenSecret: accessTokenSecret, - } -} - -// User represents the structure for a user in the Twitter API response -type User struct { - ID string `json:"id"` - Username string `json:"username"` - Name string `json:"name"` - VerifiedType string `json:"verified_type"` - ProfileImageUrl string `json:"profile_image_url"` - PublicMetrics PublicMetrics `json:"public_metrics"` -} - -// PublicMetrics represents the structure for public metrics in the Twitter API response -type PublicMetrics struct { - FollowersCount int `json:"followers_count"` - FollowingCount int `json:"following_count"` - TweetCount int `json:"tweet_count"` - LikeCount int `json:"like_count"` -} - -// Tweet represents the structure for a tweet in the Twitter API response -type Tweet struct { - ID string `json:"id"` - Text string `json:"text"` - AuthorID *string `json:"author_id"` - - AdditionalMetadata AdditionalTweetMetadata -} - -// AdditionalTweetMetadata adds additinal metadata to a tweet that isn't directly -// represented in the Twitter API response -type AdditionalTweetMetadata struct { - Author *User -} - -// GetUserById makes a request to the Twitter API and returns the user's information -// by ID -func (c *Client) GetUserById(ctx context.Context, id string) (*User, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetUserById") - defer tracer.End() - - user, err := c.getUser(ctx, baseUrl+"users/"+id) - if err != nil { - tracer.OnError(err) - } - return user, err -} - -// GetUserByUsername makes a request to the Twitter API and returns the user's information -// by username -func (c *Client) GetUserByUsername(ctx context.Context, username string) (*User, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetUserByUsername") - defer tracer.End() - - user, err := c.getUser(ctx, baseUrl+"users/by/username/"+username) - if err != nil { - tracer.OnError(err) - } - return user, err -} - -// GetUserTweets gets tweets for a given user -func (c *Client) GetUserTweets(ctx context.Context, userId string, maxResults int, nextToken *string) ([]*Tweet, *string, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetUserTweets") - defer tracer.End() - - url := fmt.Sprintf(baseUrl+"users/"+userId+"/tweets?max_results=%d", maxResults) - if nextToken != nil { - url = fmt.Sprintf("%s&next_token=%s", url, *nextToken) - } - - tweets, nextToken, err := c.getTweets(ctx, url) - if err != nil { - tracer.OnError(err) - } - return tweets, nextToken, err -} - -// SearchRecentTweets searches for recent tweets within the last 7 days matching -// a search string. -func (c *Client) SearchRecentTweets(ctx context.Context, searchString string, maxResults int, nextToken *string) ([]*Tweet, *string, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "SearchUserTweets") - defer tracer.End() - - url := fmt.Sprintf( - baseUrl+"tweets/search/recent?query=%s&expansions=author_id&user.fields=username,profile_image_url,public_metrics,verified_type&max_results=%d", - url.QueryEscape(searchString), - maxResults, - ) - if nextToken != nil { - url = fmt.Sprintf("%s&next_token=%s", url, *nextToken) - } - - tweets, nextToken, err := c.getTweets(ctx, url) - if err != nil { - tracer.OnError(err) - } - return tweets, nextToken, err -} - -// SendReply sends a reply to the provided tweet -func (c *Client) SendReply(ctx context.Context, tweetId, text string) (string, error) { - tracer := metrics.TraceMethodCall(ctx, metricsStructName, "SendReply") - defer tracer.End() - - return c.sendTweet(ctx, text, &tweetId) -} - -func (c *Client) getUser(ctx context.Context, fromUrl string) (*User, error) { - bearerToken, err := c.getBearerToken() - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", fromUrl+"?user.fields=profile_image_url,public_metrics,verified_type", nil) - if err != nil { - return nil, err - } - - req = req.WithContext(ctx) - - req.Header.Add("Authorization", "Bearer "+bearerToken) - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode) - } - - var result struct { - Data *User `json:"data"` - Errors []*twitterError `json:"errors"` - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if err := json.Unmarshal(body, &result); err != nil { - return nil, err - } - - if len(result.Errors) > 0 { - return nil, result.Errors[0].toError() - } - return result.Data, nil -} - -func (c *Client) getTweets(ctx context.Context, fromUrl string) ([]*Tweet, *string, error) { - bearerToken, err := c.getBearerToken() - if err != nil { - return nil, nil, err - } - - req, err := http.NewRequest("GET", fromUrl, nil) - if err != nil { - return nil, nil, err - } - - req = req.WithContext(ctx) - - req.Header.Add("Authorization", "Bearer "+bearerToken) - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, nil, fmt.Errorf("unexpected http status code: %d", resp.StatusCode) - } - - var result struct { - Data []*Tweet `json:"data"` - Errors []*twitterError `json:"errors"` - Meta struct { - NextToken *string `json:"next_token"` - } `json:"meta"` - Includes struct { - Users []User `json:"users"` - } `json:"includes"` - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, err - } - - if err := json.Unmarshal(body, &result); err != nil { - return nil, nil, err - } - - if len(result.Errors) > 0 { - return nil, nil, result.Errors[0].toError() - } - - for _, tweet := range result.Data { - if tweet.AuthorID == nil { - continue - } - - for _, user := range result.Includes.Users { - if user.ID == *tweet.AuthorID { - tweet.AdditionalMetadata.Author = &user - break - } - } - } - - return result.Data, result.Meta.NextToken, nil -} - -func (c *Client) sendTweet(ctx context.Context, text string, inReplyTo *string) (string, error) { - apiUrl := baseUrl + "tweets" - - type ReplyParams struct { - InReplyToTweetId string `json:"in_reply_to_tweet_id"` - } - type Request struct { - Text string `json:"text"` - Reply *ReplyParams `json:"reply"` - } - - reqPayload := Request{ - Text: text, - } - if inReplyTo != nil { - reqPayload.Reply = &ReplyParams{ - InReplyToTweetId: *inReplyTo, - } - } - - reqJson, err := json.Marshal(reqPayload) - if err != nil { - return "", err - } - - req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(reqJson)) - if err != nil { - return "", err - } - - req = req.WithContext(ctx) - - req.Header.Set("Content-Type", "application/json") - - config := oauth1.NewConfig(c.clientId, c.clientSecret) - token := oauth1.NewToken(c.accessToken, c.accessTokenSecret) - httpClient := config.Client(oauth1.NoContext, token) - - resp, err := httpClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return "", fmt.Errorf("unexpected http status code: %d", resp.StatusCode) - } - - var result struct { - Data struct { - Id *string `json:"id"` - } `json:"data"` - Errors []*twitterError `json:"errors"` - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - if err := json.Unmarshal(body, &result); err != nil { - return "", err - } - - if len(result.Errors) > 0 { - return "", result.Errors[0].toError() - } - return *result.Data.Id, nil -} - -func (c *Client) getBearerToken() (string, error) { - c.bearerTokenMu.RLock() - if time.Since(c.lastBearerTokenRefresh) < bearerTokenMaxAge { - c.bearerTokenMu.RUnlock() - return c.bearerToken, nil - } - c.bearerTokenMu.RUnlock() - - c.bearerTokenMu.Lock() - defer c.bearerTokenMu.Unlock() - - if time.Since(c.lastBearerTokenRefresh) < bearerTokenMaxAge { - return c.bearerToken, nil - } - - requestData := []byte("grant_type=client_credentials") - req, err := http.NewRequest("POST", "https://api.twitter.com/oauth2/token", bytes.NewBuffer(requestData)) - if err != nil { - return "", err - } - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(c.clientId, c.clientSecret) - - resp, err := c.httpClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - var result struct { - TokenType string `json:"token_type"` - AccessToken string `json:"access_token"` - } - - if err := json.Unmarshal(body, &result); err != nil { - return "", err - } - - if len(result.AccessToken) == 0 { - return "", fmt.Errorf("could not get access token") - } - - c.bearerToken = result.AccessToken - c.lastBearerTokenRefresh = time.Now() - - return result.AccessToken, nil -} - -// IsRetweet returns whether the tweet is a retweet -func (t *Tweet) IsRetweet() bool { - return strings.HasPrefix(t.Text, "RT @") -} - -type twitterError struct { - Title string `json:"title"` - Detail string `json:"detail"` -} - -func (e *twitterError) toError() error { - return errors.Errorf("%s: %s", e.Title, e.Detail) -} From 1d19fd778a7a921f2148867c0dffec7ac8bc6489 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 2 Apr 2025 15:33:16 -0400 Subject: [PATCH 79/79] Pull latest code-protobuf-api --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bac76e90..470acfad 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.0 require ( github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.18.1-0.20250402182022-037f36525341 + github.com/code-payments/code-protobuf-api v1.19.1-0.20250402192552-177b85d5508d github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.2.1 diff --git a/go.sum b/go.sum index b3dad4be..84945daa 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.18.1-0.20250402182022-037f36525341 h1:kmB+HffbKF0BoJSTudH21IXTZy+PFPyukqWJIjppUkE= -github.com/code-payments/code-protobuf-api v1.18.1-0.20250402182022-037f36525341/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs= +github.com/code-payments/code-protobuf-api v1.19.1-0.20250402192552-177b85d5508d h1:p0XvGezrScoG4CLGWbV702qK8MOwNSOPxfumVJ/kqBc= +github.com/code-payments/code-protobuf-api v1.19.1-0.20250402192552-177b85d5508d/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs= github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba h1:Bkp+gmeb6Y2PWXfkSCTMBGWkb2P1BujRDSjWeI+0j5I= github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba/go.mod h1:jSiifpiBpyBQ8q2R0MGEbkSgWC6sbdRTyDBntmW+j1E= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw=