diff --git a/go.mod b/go.mod index 1d468327..9c0548ac 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( filippo.io/edwards25519 v1.1.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.19.1-0.20250827160012-3edaffb82d79 + github.com/code-payments/code-protobuf-api v1.19.1-0.20250909140022-32d989862f5a 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 e00984c7..02c006d8 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,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.19.1-0.20250827160012-3edaffb82d79 h1:Td/jOwNQCuSaTL6NZstWSMxgjcyVhkC1Q/BmVRqY6qo= -github.com/code-payments/code-protobuf-api v1.19.1-0.20250827160012-3edaffb82d79/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs= +github.com/code-payments/code-protobuf-api v1.19.1-0.20250909140022-32d989862f5a h1:KJHqqNz1gEOhjg97mw1B81Fvd4CClWeaSJw2AMOqSkA= +github.com/code-payments/code-protobuf-api v1.19.1-0.20250909140022-32d989862f5a/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= diff --git a/pkg/code/aml/guard_test.go b/pkg/code/aml/guard_test.go index 2082f1d0..7957bcf7 100644 --- a/pkg/code/aml/guard_test.go +++ b/pkg/code/aml/guard_test.go @@ -176,6 +176,8 @@ func makeSendPublicPaymentIntent(t *testing.T, owner *common.Account, usdMarketV IsWithdrawal: isWithdraw, }, + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), + InitiatorOwnerAccount: owner.PublicKey().ToBase58(), State: intent.StatePending, diff --git a/pkg/code/async/account/gift_card.go b/pkg/code/async/account/gift_card.go index 61451794..25f1c9a0 100644 --- a/pkg/code/async/account/gift_card.go +++ b/pkg/code/async/account/gift_card.go @@ -17,12 +17,12 @@ import ( "github.com/code-payments/code-server/pkg/code/balance" "github.com/code-payments/code-server/pkg/code/common" + currency_util "github.com/code-payments/code-server/pkg/code/currency" 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/currency" "github.com/code-payments/code-server/pkg/metrics" "github.com/code-payments/code-server/pkg/pointer" "github.com/code-payments/code-server/pkg/retry" @@ -300,7 +300,12 @@ func updateAutoReturnFulfillmentPreSorting( } func insertAutoReturnIntentRecord(ctx context.Context, data code_data.Provider, giftCardIssuedIntent *intent.Record, isVoidedByUser bool) error { - usdExchangeRecord, err := data.GetExchangeRate(ctx, currency.USD, time.Now()) + mintAccount, err := common.NewAccountFromPublicKeyString(giftCardIssuedIntent.MintAccount) + if err != nil { + return err + } + + usdMarketValue, err := currency_util.CalculateUsdMarketValue(ctx, data, mintAccount, giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity, time.Now()) if err != nil { return err } @@ -312,6 +317,8 @@ func insertAutoReturnIntentRecord(ctx context.Context, data code_data.Provider, IntentId: getAutoReturnIntentId(giftCardIssuedIntent.IntentId), IntentType: intent.ReceivePaymentsPublicly, + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), + InitiatorOwnerAccount: giftCardIssuedIntent.InitiatorOwnerAccount, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{ @@ -326,7 +333,7 @@ func insertAutoReturnIntentRecord(ctx context.Context, data code_data.Provider, OriginalExchangeRate: giftCardIssuedIntent.SendPublicPaymentMetadata.ExchangeRate, OriginalNativeAmount: giftCardIssuedIntent.SendPublicPaymentMetadata.NativeAmount, - UsdMarketValue: usdExchangeRecord.Rate * float64(giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity) / float64(common.CoreMintQuarksPerUnit), + UsdMarketValue: usdMarketValue, }, State: intent.StateConfirmed, diff --git a/pkg/code/async/account/service.go b/pkg/code/async/account/service.go index f864a264..5294ab93 100644 --- a/pkg/code/async/account/service.go +++ b/pkg/code/async/account/service.go @@ -65,6 +65,11 @@ func (p *service) mustLoadAirdropper(ctx context.Context) { }) err := func() error { + vmConfig, err := common.GetVmConfigForMint(ctx, p.data, common.CoreMintAccount) + if err != nil { + return err + } + vaultRecord, err := p.data.GetKey(ctx, p.conf.airdropperOwnerPublicKey.Get(ctx)) if err != nil { return err @@ -75,7 +80,7 @@ func (p *service) mustLoadAirdropper(ctx context.Context) { return err } - timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmConfig) if err != nil { return err } diff --git a/pkg/code/async/account/testutil.go b/pkg/code/async/account/testutil.go index 9103133a..0da9ae6f 100644 --- a/pkg/code/async/account/testutil.go +++ b/pkg/code/async/account/testutil.go @@ -18,6 +18,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/pointer" "github.com/code-payments/code-server/pkg/testutil" ) @@ -57,10 +58,11 @@ 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(vm, common.CoreMintAccount) + vmConfig := testutil.NewRandomVmConfig(t, true) + + timelockAccounts, err := authority.GetTimelockAccounts(vmConfig) require.NoError(t, err) accountInfoRecord := &account.Record{ @@ -81,13 +83,15 @@ func (e *testEnv) generateRandomGiftCard(t *testing.T, creationTs time.Time) *te IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), IntentType: intent.SendPublicPayment, + MintAccount: vmConfig.Mint.PublicKey().ToBase58(), + InitiatorOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ DestinationTokenAccount: accountInfoRecord.TokenAccount, Quantity: common.ToCoreMintQuarks(12345), - ExchangeCurrency: common.CoreMintSymbol, + ExchangeCurrency: currency_lib.USD, ExchangeRate: 1.0, NativeAmount: 12345, UsdMarketValue: 1000.0, diff --git a/pkg/code/async/airdrop/config.go b/pkg/code/async/airdrop/config.go deleted file mode 100644 index 0bfcc7c5..00000000 --- a/pkg/code/async/airdrop/config.go +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 224ca75b..00000000 --- a/pkg/code/async/airdrop/indexer.go +++ /dev/null @@ -1,42 +0,0 @@ -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" -) - -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()}, - Owner: &indexerpb.Address{Value: owner.PublicKey().ToBytes()}, - }) - 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()) - } - - 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 deleted file mode 100644 index ef37d6c1..00000000 --- a/pkg/code/async/airdrop/integration.go +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index c061fbbc..00000000 --- a/pkg/code/async/airdrop/nonce.go +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index a16257d5..00000000 --- a/pkg/code/async/airdrop/service.go +++ /dev/null @@ -1,122 +0,0 @@ -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.CoreMintAccount) - 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 deleted file mode 100644 index e060585a..00000000 --- a/pkg/code/async/airdrop/transaction.go +++ /dev/null @@ -1,244 +0,0 @@ -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 = 150 - - 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 == errNotIndexed { - continue - } else 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 { - usdMarketValue := 0.001 - 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.CoreMintAccount) - 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 deleted file mode 100644 index 89ad4c5b..00000000 --- a/pkg/code/async/airdrop/worker.go +++ /dev/null @@ -1,63 +0,0 @@ -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 -} diff --git a/pkg/code/async/geyser/external_deposit.go b/pkg/code/async/geyser/external_deposit.go index 8727f94c..38059381 100644 --- a/pkg/code/async/geyser/external_deposit.go +++ b/pkg/code/async/geyser/external_deposit.go @@ -17,11 +17,11 @@ import ( "github.com/code-payments/code-server/pkg/cache" "github.com/code-payments/code-server/pkg/code/common" + currency_util "github.com/code-payments/code-server/pkg/code/currency" code_data "github.com/code-payments/code-server/pkg/code/data" "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/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/retry" "github.com/code-payments/code-server/pkg/solana" @@ -65,7 +65,12 @@ func fixMissingExternalDeposits(ctx context.Context, data code_data.Provider, vm } func maybeInitiateExternalDepositIntoVm(ctx context.Context, data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, userAuthority *common.Account) error { - vmDepositAccounts, err := userAuthority.GetVmDepositAccounts(common.CodeVmAccount, common.CoreMintAccount) + vmConfig, err := common.GetVmConfigForMint(ctx, data, common.CoreMintAccount) + if err != nil { + return err + } + + vmDepositAccounts, err := userAuthority.GetVmDepositAccounts(vmConfig) if err != nil { return errors.Wrap(err, "error getting vm deposit ata") } @@ -84,7 +89,12 @@ func maybeInitiateExternalDepositIntoVm(ctx context.Context, data code_data.Prov } func initiateExternalDepositIntoVm(ctx context.Context, data code_data.Provider, vmIndexerClient indexerpb.IndexerClient, userAuthority *common.Account, balance uint64) error { - vmDepositAccounts, err := userAuthority.GetVmDepositAccounts(common.CodeVmAccount, common.CoreMintAccount) + vmConfig, err := common.GetVmConfigForMint(ctx, data, common.CoreMintAccount) + if err != nil { + return err + } + + vmDepositAccounts, err := userAuthority.GetVmDepositAccounts(vmConfig) if err != nil { return errors.Wrap(err, "error getting vm deposit ata") } @@ -150,7 +160,12 @@ func initiateExternalDepositIntoVm(ctx context.Context, data code_data.Provider, } func findPotentialExternalDepositsIntoVm(ctx context.Context, data code_data.Provider, userAuthority *common.Account) ([]string, error) { - vmDepositAta, err := userAuthority.ToVmDepositAssociatedTokenAccount(common.CodeVmAccount, common.CoreMintAccount) + vmConfig, err := common.GetVmConfigForMint(ctx, data, common.CoreMintAccount) + if err != nil { + return nil, err + } + + vmDepositAta, err := userAuthority.ToVmDepositAta(vmConfig) if err != nil { return nil, errors.Wrap(err, "error getting vm deposit ata") } @@ -204,7 +219,12 @@ func findPotentialExternalDepositsIntoVm(ctx context.Context, data code_data.Pro } func processPotentialExternalDepositIntoVm(ctx context.Context, data code_data.Provider, integration Integration, signature string, userAuthority *common.Account) error { - vmDepositAta, err := userAuthority.ToVmDepositAssociatedTokenAccount(common.CodeVmAccount, common.CoreMintAccount) + vmConfig, err := common.GetVmConfigForMint(ctx, data, common.CoreMintAccount) + if err != nil { + return err + } + + vmDepositAta, err := userAuthority.ToVmDepositAta(vmConfig) if err != nil { return errors.Wrap(err, "error getting vm deposit ata") } @@ -288,11 +308,10 @@ func processPotentialExternalDepositIntoVm(ctx context.Context, data code_data.P return errors.Wrap(err, "invalid owner account") } - usdExchangeRecord, err := data.GetExchangeRate(ctx, currency_lib.USD, time.Now()) + usdMarketValue, err := currency_util.CalculateUsdMarketValue(ctx, data, common.CoreMintAccount, uint64(deltaQuarksIntoOmnibus), time.Now()) if err != nil { - return errors.Wrap(err, "error getting usd rate") + return errors.Wrap(err, "error calculating usd market value") } - usdMarketValue := usdExchangeRecord.Rate * float64(deltaQuarksIntoOmnibus) / float64(common.CoreMintQuarksPerUnit) err = data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { // For transaction history @@ -300,6 +319,8 @@ func processPotentialExternalDepositIntoVm(ctx context.Context, data code_data.P IntentId: getExternalDepositIntentID(signature, userVirtualTimelockVaultAccount), IntentType: intent.ExternalDeposit, + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), + InitiatorOwnerAccount: ownerAccount.PublicKey().ToBase58(), ExternalDepositMetadata: &intent.ExternalDepositMetadata{ diff --git a/pkg/code/async/geyser/timelock.go b/pkg/code/async/geyser/timelock.go index aa95de8f..fb5b52e5 100644 --- a/pkg/code/async/geyser/timelock.go +++ b/pkg/code/async/geyser/timelock.go @@ -36,12 +36,27 @@ func updateTimelockAccountRecord(ctx context.Context, data code_data.Provider, t } func getTimelockUnlockState(ctx context.Context, data code_data.Provider, timelockRecord *timelock.Record) (*cvm.UnlockStateAccount, uint64, error) { - ownerAccount, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) + accountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, timelockRecord.VaultAddress) if err != nil { return nil, 0, err } - timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + vaultOwnerAccount, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) + if err != nil { + return nil, 0, err + } + + mintAccount, err := common.NewAccountFromPublicKeyString(accountInfoRecord.MintAccount) + if err != nil { + return nil, 0, err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, data, mintAccount) + if err != nil { + return nil, 0, err + } + + timelockAccounts, err := vaultOwnerAccount.GetTimelockAccounts(vmConfig) if err != nil { return nil, 0, err } diff --git a/pkg/code/async/sequencer/config.go b/pkg/code/async/sequencer/config.go index a5e1420d..073ea34b 100644 --- a/pkg/code/async/sequencer/config.go +++ b/pkg/code/async/sequencer/config.go @@ -19,18 +19,18 @@ const ( MaxGlobalFailedFulfillmentsConfigEnvName = envConfigPrefix + "MAX_GLOBAL_FAILED_FULFILLMENTS" defaultMaxGlobalFailedFulfillments = 10 - //FulfillmentBatchSizeConfigEnvName = envConfigPrefix + "WORKER_BATCH_SIZE" - //defaultFulfillmentBatchSize = 100 + FulfillmentBatchSizeConfigEnvName = envConfigPrefix + "WORKER_BATCH_SIZE" + defaultFulfillmentBatchSize = 100 EnableSubsidizerChecksConfigEnvName = envConfigPrefix + "ENABLE_SUBSIDIZER_CHECKS" defaultEnableSubsidizerChecks = true ) type conf struct { - disableTransactionScheduling config.Bool - disableTransactionSubmission config.Bool - maxGlobalFailedFulfillments config.Uint64 - //fulfillmentBatchSize config.Uint64 + disableTransactionScheduling config.Bool + disableTransactionSubmission config.Bool + maxGlobalFailedFulfillments config.Uint64 + fulfillmentBatchSize config.Uint64 enableSubsidizerChecks config.Bool enableCachedTransactionLookup config.Bool } @@ -42,10 +42,10 @@ type ConfigProvider func() *conf func WithEnvConfigs() ConfigProvider { return func() *conf { return &conf{ - disableTransactionScheduling: env.NewBoolConfig(DisableTransactionSchedulingConfigEnvName, defaultDisableTransactionScheduling), - disableTransactionSubmission: env.NewBoolConfig(DisableTransactionSubmissionConfigEnvName, defaultDisableTransactionSubmission), - maxGlobalFailedFulfillments: env.NewUint64Config(MaxGlobalFailedFulfillmentsConfigEnvName, defaultMaxGlobalFailedFulfillments), - //fulfillmentBatchSize: env.NewUint64Config(FulfillmentBatchSizeConfigEnvName, defaultFulfillmentBatchSize), + disableTransactionScheduling: env.NewBoolConfig(DisableTransactionSchedulingConfigEnvName, defaultDisableTransactionScheduling), + disableTransactionSubmission: env.NewBoolConfig(DisableTransactionSubmissionConfigEnvName, defaultDisableTransactionSubmission), + maxGlobalFailedFulfillments: env.NewUint64Config(MaxGlobalFailedFulfillmentsConfigEnvName, defaultMaxGlobalFailedFulfillments), + fulfillmentBatchSize: env.NewUint64Config(FulfillmentBatchSizeConfigEnvName, defaultFulfillmentBatchSize), enableSubsidizerChecks: env.NewBoolConfig(EnableSubsidizerChecksConfigEnvName, defaultEnableSubsidizerChecks), enableCachedTransactionLookup: wrapper.NewBoolConfig(memory.NewConfig(false), false), } @@ -60,10 +60,10 @@ type testOverrides struct { func withManualTestOverrides(overrides *testOverrides) ConfigProvider { return func() *conf { return &conf{ - disableTransactionScheduling: wrapper.NewBoolConfig(memory.NewConfig(overrides.disableTransactionScheduling), defaultDisableTransactionScheduling), - disableTransactionSubmission: wrapper.NewBoolConfig(memory.NewConfig(true), defaultDisableTransactionSubmission), - maxGlobalFailedFulfillments: wrapper.NewUint64Config(memory.NewConfig(overrides.maxGlobalFailedFulfillments), defaultMaxGlobalFailedFulfillments), - //fulfillmentBatchSize: wrapper.NewUint64Config(memory.NewConfig(defaultFulfillmentBatchSize), defaultFulfillmentBatchSize), + disableTransactionScheduling: wrapper.NewBoolConfig(memory.NewConfig(overrides.disableTransactionScheduling), defaultDisableTransactionScheduling), + disableTransactionSubmission: wrapper.NewBoolConfig(memory.NewConfig(true), defaultDisableTransactionSubmission), + maxGlobalFailedFulfillments: wrapper.NewUint64Config(memory.NewConfig(overrides.maxGlobalFailedFulfillments), defaultMaxGlobalFailedFulfillments), + fulfillmentBatchSize: wrapper.NewUint64Config(memory.NewConfig(defaultFulfillmentBatchSize), defaultFulfillmentBatchSize), enableSubsidizerChecks: wrapper.NewBoolConfig(memory.NewConfig(false), defaultEnableSubsidizerChecks), enableCachedTransactionLookup: wrapper.NewBoolConfig(memory.NewConfig(true), true), } diff --git a/pkg/code/async/sequencer/fulfillment_handler.go b/pkg/code/async/sequencer/fulfillment_handler.go index 2365a69d..9c09038a 100644 --- a/pkg/code/async/sequencer/fulfillment_handler.go +++ b/pkg/code/async/sequencer/fulfillment_handler.go @@ -44,10 +44,16 @@ type FulfillmentHandler interface { SupportsOnDemandTransactions() bool // MakeOnDemandTransaction constructs a transaction at the time of submission - // to the blockchain. This is an optimization for the nonce pool. Implementations - // should not modify the provided fulfillment record or selected nonce, but rather - // use relevant fields to make the corresponding transaction. - MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.Nonce) (*solana.Transaction, error) + // to the blockchain, which provides an optimization for the nonce pool. Any + // additional signers for the transaction should also be returned. + // + // Note: It's not required, but also safe, to return the subsidizer account as + // a signer. + // + // Note: Implementations should not modify the provided fulfillment record or + // selected nonce, but rather use relevant fields to make the corresponding + // transaction. + MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedSolanaNonce *transaction_util.Nonce) (*solana.Transaction, []*common.Account, error) // OnSuccess is a callback function executed on a finalized transaction. OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error @@ -126,34 +132,54 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) SupportsOnDemandTran return true } -func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.Nonce) (*solana.Transaction, error) { +func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedSolanaNonce *transaction_util.Nonce) (*solana.Transaction, []*common.Account, error) { if fulfillmentRecord.FulfillmentType != fulfillment.InitializeLockedTimelockAccount { - return nil, errors.New("invalid fulfillment type") + return nil, nil, errors.New("invalid fulfillment type") } timelockRecord, err := h.data.GetTimelockByVault(ctx, fulfillmentRecord.Source) if err != nil { - return nil, err + return nil, nil, err + } + + accountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, fulfillmentRecord.Source) + if err != nil { + return nil, nil, err + } + + mint, err := common.NewAccountFromPublicKeyString(accountInfoRecord.MintAccount) + if err != nil { + return nil, nil, err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, h.data, mint) + if err != nil { + return nil, nil, err } - authorityAccount, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) + authority, err := common.NewAccountFromPublicKeyString(accountInfoRecord.AuthorityAccount) if err != nil { - return nil, err + return nil, nil, err } - timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(vmConfig) if err != nil { - return nil, err + return nil, nil, err } - memory, accountIndex, err := reserveVmMemory(ctx, h.data, common.CodeVmAccount, cvm.VirtualAccountTypeTimelock, timelockAccounts.Vault) + if timelockRecord.VaultAddress != timelockAccounts.Vault.PublicKey().ToBase58() { + return nil, nil, errors.New("unexpected timelock vault address") + } + + memory, accountIndex, err := reserveVmMemory(ctx, h.data, vmConfig.Vm, cvm.VirtualAccountTypeTimelock, timelockAccounts.Vault) if err != nil { - return nil, err + return nil, nil, err } txn, err := transaction_util.MakeOpenAccountTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + selectedSolanaNonce, + + vmConfig, memory, accountIndex, @@ -161,9 +187,9 @@ func (h *InitializeLockedTimelockAccountFulfillmentHandler) MakeOnDemandTransact timelockAccounts, ) if err != nil { - return nil, err + return nil, nil, err } - return &txn, nil + return &txn, []*common.Account{vmConfig.Authority}, nil } func (h *InitializeLockedTimelockAccountFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { @@ -286,86 +312,98 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) SupportsOnDemandTrans return true } -func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.Nonce) (*solana.Transaction, error) { +func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedSolanaNonce *transaction_util.Nonce) (*solana.Transaction, []*common.Account, error) { actionRecord, err := h.data.GetActionById(ctx, fulfillmentRecord.Intent, fulfillmentRecord.ActionId) if err != nil { - return nil, err + return nil, nil, err } virtualSignatureBytes, err := base58.Decode(*fulfillmentRecord.VirtualSignature) if err != nil { - return nil, err + return nil, nil, err } virtualNonce, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.VirtualNonce) if err != nil { - return nil, err + return nil, nil, err } sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) if err != nil { - return nil, err + return nil, nil, err } sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, sourceVault.PublicKey().ToBase58()) if err != nil { - return nil, err + return nil, nil, err } sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) if err != nil { - return nil, err + return nil, nil, err } - destinationTokenAccount, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) + mint, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.MintAccount) if err != nil { - return nil, err + return nil, nil, err } - _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) + vmConfig, err := common.GetVmConfigForMint(ctx, h.data, mint) if err != nil { - return nil, err + return nil, nil, err } - _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, sourceAuthority) + destinationToken, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) if err != nil { - return nil, err + return nil, nil, err } - isInternal, err := isInternalVmTransfer(ctx, h.data, destinationTokenAccount) + _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, vmConfig.Vm, virtualNonce) if err != nil { - return nil, err + return nil, nil, err + } + + _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vmConfig.Vm, sourceAuthority) + if err != nil { + return nil, nil, err + } + + isInternal, err := isInternalVmTransfer(ctx, h.data, destinationToken) + if err != nil { + return nil, nil, err } var txn solana.Transaction var makeTxnErr error if isInternal { - destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationTokenAccount.PublicKey().ToBase58()) + destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationToken.PublicKey().ToBase58()) if err != nil { - return nil, err + return nil, nil, err } destinationAuthority, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) if err != nil { - return nil, err + return nil, nil, err } - _, destinationMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationAuthority) + _, destinationMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vmConfig.Vm, destinationAuthority) if err != nil { - return nil, err + return nil, nil, err } txn, makeTxnErr = transaction_util.MakeInternalTransferWithAuthorityTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + selectedSolanaNonce, + + vmConfig, solana.Signature(virtualSignatureBytes), - common.CodeVmAccount, nonceMemory, nonceIndex, + sourceMemory, sourceIndex, + destinationMemory, destinationIndex, @@ -380,48 +418,47 @@ func (h *NoPrivacyTransferWithAuthorityFulfillmentHandler) MakeOnDemandTransacti if !isFeePayment { isCreateOnSend, err = h.data.HasFeeAction(ctx, fulfillmentRecord.Intent, transactionpb.FeePaymentAction_CREATE_ON_SEND_WITHDRAWAL) if err != nil { - return &solana.Transaction{}, err + return nil, nil, err } } - var destinationOwnerAccount *common.Account + var destinationOwner *common.Account if isCreateOnSend { intentRecord, err := h.data.GetIntent(ctx, fulfillmentRecord.Intent) if err != nil { - return nil, err + return nil, nil, err } - destinationOwnerAccount, err = common.NewAccountFromPublicKeyString(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) + destinationOwner, err = common.NewAccountFromPublicKeyString(intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount) if err != nil { - return nil, err + return nil, nil, err } } txn, makeTxnErr = transaction_util.MakeExternalTransferWithAuthorityTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + selectedSolanaNonce, - solana.Signature(virtualSignatureBytes), + vmConfig, - common.CodeVmAccount, - common.CodeVmOmnibusAccount, + solana.Signature(virtualSignatureBytes), nonceMemory, nonceIndex, sourceMemory, sourceIndex, - isCreateOnSend, - destinationOwnerAccount, - destinationTokenAccount, + destinationOwner, + destinationToken, + isCreateOnSend, + mint, *actionRecord.Quantity, ) } if makeTxnErr != nil { - return nil, makeTxnErr + return nil, nil, makeTxnErr } - return &txn, nil + return &txn, []*common.Account{vmConfig.Authority}, nil } type NoPrivacyWithdrawFulfillmentHandler struct { @@ -490,106 +527,117 @@ func (h *NoPrivacyWithdrawFulfillmentHandler) SupportsOnDemandTransactions() boo return true } -func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.Nonce) (*solana.Transaction, error) { +func (h *NoPrivacyWithdrawFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedSolanaNonce *transaction_util.Nonce) (*solana.Transaction, []*common.Account, error) { virtualSignatureBytes, err := base58.Decode(*fulfillmentRecord.VirtualSignature) if err != nil { - return nil, err + return nil, nil, err } virtualNonce, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.VirtualNonce) if err != nil { - return nil, err + return nil, nil, err } sourceVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) if err != nil { - return nil, err + return nil, nil, err } sourceAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, sourceVault.PublicKey().ToBase58()) if err != nil { - return nil, err + return nil, nil, err } sourceAuthority, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.AuthorityAccount) if err != nil { - return nil, err + return nil, nil, err } - destinationTokenAccount, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) + mint, err := common.NewAccountFromPublicKeyString(sourceAccountInfoRecord.MintAccount) if err != nil { - return nil, err + return nil, nil, err } - _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, virtualNonce) + vmConfig, err := common.GetVmConfigForMint(ctx, h.data, mint) if err != nil { - return nil, err + return nil, nil, err } - _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, sourceAuthority) + destinationToken, err := common.NewAccountFromPublicKeyString(*fulfillmentRecord.Destination) if err != nil { - return nil, err + return nil, nil, err } - isInternal, err := isInternalVmTransfer(ctx, h.data, destinationTokenAccount) + _, nonceMemory, nonceIndex, err := getVirtualDurableNonceAccountStateInMemory(ctx, h.vmIndexerClient, vmConfig.Vm, virtualNonce) if err != nil { - return nil, err + return nil, nil, err + } + + _, sourceMemory, sourceIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vmConfig.Vm, sourceAuthority) + if err != nil { + return nil, nil, err + } + + isInternal, err := isInternalVmTransfer(ctx, h.data, destinationToken) + if err != nil { + return nil, nil, err } var txn solana.Transaction var makeTxnErr error if isInternal { - destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationTokenAccount.PublicKey().ToBase58()) + destinationAccountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, destinationToken.PublicKey().ToBase58()) if err != nil { - return nil, err + return nil, nil, err } destinationAuthority, err := common.NewAccountFromPublicKeyString(destinationAccountInfoRecord.AuthorityAccount) if err != nil { - return nil, err + return nil, nil, err } - _, destinationMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, destinationAuthority) + _, destinationMemory, destinationIndex, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vmConfig.Vm, destinationAuthority) if err != nil { - return nil, err + return nil, nil, err } txn, makeTxnErr = transaction_util.MakeInternalWithdrawTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + selectedSolanaNonce, + + vmConfig, solana.Signature(virtualSignatureBytes), - common.CodeVmAccount, nonceMemory, nonceIndex, + sourceMemory, sourceIndex, + destinationMemory, destinationIndex, ) } else { txn, makeTxnErr = transaction_util.MakeExternalWithdrawTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + selectedSolanaNonce, - solana.Signature(virtualSignatureBytes), + vmConfig, - common.CodeVmAccount, - common.CodeVmOmnibusAccount, + solana.Signature(virtualSignatureBytes), nonceMemory, nonceIndex, + sourceMemory, sourceIndex, - destinationTokenAccount, + destinationToken, ) } if makeTxnErr != nil { - return nil, makeTxnErr + return nil, nil, makeTxnErr } - return &txn, nil + return &txn, []*common.Account{vmConfig.Authority}, nil } func (h *NoPrivacyWithdrawFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { @@ -680,52 +728,66 @@ func (h *CloseEmptyTimelockAccountFulfillmentHandler) SupportsOnDemandTransactio return true } -func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.Nonce) (*solana.Transaction, error) { +func (h *CloseEmptyTimelockAccountFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedSolanaNonce *transaction_util.Nonce) (*solana.Transaction, []*common.Account, error) { if fulfillmentRecord.FulfillmentType != fulfillment.CloseEmptyTimelockAccount { - return nil, errors.New("invalid fulfillment type") + return nil, nil, errors.New("invalid fulfillment type") } timelockVault, err := common.NewAccountFromPublicKeyString(fulfillmentRecord.Source) if err != nil { - return nil, err + return nil, nil, err } - timelockRecord, err := h.data.GetTimelockByVault(ctx, timelockVault.PublicKey().ToBase58()) + + accountInfoRecord, err := h.data.GetAccountInfoByTokenAddress(ctx, timelockVault.PublicKey().ToBase58()) if err != nil { - return nil, err + return nil, nil, err } - timelockOwner, err := common.NewAccountFromPublicKeyString(timelockRecord.VaultOwner) + + mint, err := common.NewAccountFromPublicKeyString(accountInfoRecord.MintAccount) + if err != nil { + return nil, nil, err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, h.data, mint) if err != nil { - return nil, err + return nil, nil, err } - virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, common.CodeVmAccount, timelockOwner) + timelockOwner, err := common.NewAccountFromPublicKeyString(accountInfoRecord.AuthorityAccount) if err != nil { - return nil, err + return nil, nil, err + } + + virtualAccountState, memory, index, err := getVirtualTimelockAccountStateInMemory(ctx, h.vmIndexerClient, vmConfig.Vm, timelockOwner) + if err != nil { + return nil, nil, err } if virtualAccountState.Balance != 0 { - return nil, errors.New("stale timelock account state") + return nil, nil, errors.New("stale timelock account state") } - storage, err := reserveVmStorage(ctx, h.data, common.CodeVmAccount, storage.PurposeDeletion, timelockVault) + storage, err := reserveVmStorage(ctx, h.data, vmConfig.Vm, storage.PurposeDeletion, timelockVault) if err != nil { - return nil, err + return nil, nil, err } txn, err := transaction_util.MakeCompressAccountTransaction( - selectedNonce.Account, - selectedNonce.Blockhash, + selectedSolanaNonce, + + vmConfig, - common.CodeVmAccount, memory, index, + storage, + virtualAccountState.Marshal(), ) if err != nil { - return nil, err + return nil, nil, err } - return &txn, nil + return &txn, []*common.Account{vmConfig.Authority}, nil } func (h *CloseEmptyTimelockAccountFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, txnRecord *transaction.Record) error { diff --git a/pkg/code/async/sequencer/service.go b/pkg/code/async/sequencer/service.go index acc1c601..dff85fe1 100644 --- a/pkg/code/async/sequencer/service.go +++ b/pkg/code/async/sequencer/service.go @@ -30,14 +30,14 @@ type service struct { data code_data.Provider scheduler Scheduler vmIndexerClient indexerpb.IndexerClient - noncePool *transaction.LocalNoncePool + solanaNoncePool *transaction.LocalNoncePool fulfillmentHandlersByType map[fulfillment.Type]FulfillmentHandler actionHandlersByType map[action.Type]ActionHandler intentHandlersByType map[intent.Type]IntentHandler } -func New(data code_data.Provider, scheduler Scheduler, vmIndexerClient indexerpb.IndexerClient, noncePool *transaction.LocalNoncePool, configProvider ConfigProvider) (async.Service, error) { - if err := noncePool.Validate(nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeOnDemandTransaction); err != nil { +func New(data code_data.Provider, scheduler Scheduler, vmIndexerClient indexerpb.IndexerClient, solanaNoncePool *transaction.LocalNoncePool, configProvider ConfigProvider) (async.Service, error) { + if err := solanaNoncePool.Validate(nonce.EnvironmentSolana, nonce.EnvironmentInstanceSolanaMainnet, nonce.PurposeOnDemandTransaction); err != nil { return nil, err } @@ -47,7 +47,7 @@ func New(data code_data.Provider, scheduler Scheduler, vmIndexerClient indexerpb data: data, scheduler: scheduler, vmIndexerClient: vmIndexerClient, - noncePool: noncePool, // todo: validate configuration + solanaNoncePool: solanaNoncePool, fulfillmentHandlersByType: getFulfillmentHandlers(data, vmIndexerClient), actionHandlersByType: getActionHandlers(data), intentHandlersByType: getIntentHandlers(data), diff --git a/pkg/code/async/sequencer/testutil.go b/pkg/code/async/sequencer/testutil.go index 8897fd48..60e422b8 100644 --- a/pkg/code/async/sequencer/testutil.go +++ b/pkg/code/async/sequencer/testutil.go @@ -42,14 +42,13 @@ func (h *mockFulfillmentHandler) SupportsOnDemandTransactions() bool { return h.supportsOnDemandTxnCreation } -func (h *mockFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.Nonce) (*solana.Transaction, error) { +func (h *mockFulfillmentHandler) MakeOnDemandTransaction(ctx context.Context, fulfillmentRecord *fulfillment.Record, selectedNonce *transaction_util.Nonce) (*solana.Transaction, []*common.Account, error) { if !h.supportsOnDemandTxnCreation { - return nil, errors.New("not supported") + return nil, nil, errors.New("not supported") } txn := solana.NewTransaction(common.GetSubsidizer().PublicKey().ToBytes(), memo.Instruction(selectedNonce.Account.PublicKey().ToBase58())) - txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) - return &txn, nil + return &txn, nil, nil } func (h *mockFulfillmentHandler) OnSuccess(ctx context.Context, fulfillmentRecord *fulfillment.Record, transactionRecord *transaction.Record) error { diff --git a/pkg/code/async/sequencer/worker.go b/pkg/code/async/sequencer/worker.go index c2f1ebdf..443e7830 100644 --- a/pkg/code/async/sequencer/worker.go +++ b/pkg/code/async/sequencer/worker.go @@ -1,7 +1,9 @@ package async_sequencer import ( + "bytes" "context" + "crypto/ed25519" "database/sql" "sync" "time" @@ -17,6 +19,7 @@ import ( "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" ) func (p *service) worker(serviceCtx context.Context, state fulfillment.State, interval time.Duration) error { @@ -32,21 +35,12 @@ func (p *service) worker(serviceCtx context.Context, state fulfillment.State, in defer m.End() tracedCtx := newrelic.NewContext(serviceCtx, m) - // todo: proper config to tune states individually - var limit uint64 - switch state { - case fulfillment.StatePending: - limit = 100 // todo: we'll likely want to up this one, but also rate limit our send/getSignature RPC calls - default: - limit = 100 - } - // Get a batch of records in similar state (e.g. newly created, released, reserved, etc...) items, err := p.data.GetAllFulfillmentsByState( tracedCtx, state, false, // Don't poll for fulfillments that have active scheduling disabled - query.WithLimit(limit), + query.WithLimit(p.conf.fulfillmentBatchSize.Get(serviceCtx)), query.WithCursor(cursor), ) if err != nil { @@ -223,31 +217,49 @@ func (p *service) handlePending(ctx context.Context, record *fulfillment.Record) return errors.New("unexpected scheduled fulfillment without transaction data") } - selectedNonce, err := p.noncePool.GetNonce(ctx) + selectedSolanaNonce, err := p.solanaNoncePool.GetNonce(ctx) if err != nil { return err } defer func() { - selectedNonce.ReleaseIfNotReserved(ctx) + selectedSolanaNonce.ReleaseIfNotReserved(ctx) }() err = p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { - txn, err := fulfillmentHandler.MakeOnDemandTransaction(ctx, record, selectedNonce) + txn, signers, err := fulfillmentHandler.MakeOnDemandTransaction(ctx, record, selectedSolanaNonce) if err != nil { return err } - err = txn.Sign(common.GetSubsidizer().PrivateKey().ToBytes()) + signerPrivateKeys := []ed25519.PrivateKey{common.GetSubsidizer().PrivateKey().ToBytes()} + for _, signer := range signers { + if signer.PrivateKey() == nil { + return errors.New("signer private key not provided") + } + + if !bytes.Equal(signer.PrivateKey().ToBytes(), signerPrivateKeys[0]) { + signerPrivateKeys = append(signerPrivateKeys, signer.PrivateKey().ToBytes()) + } + } + + err = txn.Sign(signerPrivateKeys...) if err != nil { return err } + var emptySignature solana.Signature + for _, signature := range txn.Signatures { + if bytes.Equal(signature[:], emptySignature[:]) { + return errors.New("signature is missing") + } + } + 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.Nonce = pointer.String(selectedSolanaNonce.Account.PublicKey().ToBase58()) + record.Blockhash = pointer.String(base58.Encode(selectedSolanaNonce.Blockhash[:])) record.Data = txn.Marshal() - err = selectedNonce.MarkReservedWithSignature(ctx, *record.Signature) + err = selectedSolanaNonce.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 ec16bc0b..323af9fe 100644 --- a/pkg/code/async/sequencer/worker_test.go +++ b/pkg/code/async/sequencer/worker_test.go @@ -26,6 +26,7 @@ import ( "github.com/code-payments/code-server/pkg/testutil" ) +// todo: include tests for additional signers beyond subsidizer // todo: include new virtual nonce account handling tests func TestFulfillmentWorker_StateUnknown_RemainInStateUnknown(t *testing.T) { diff --git a/pkg/code/async/vm/service.go b/pkg/code/async/vm/service.go deleted file mode 100644 index 3505beef..00000000 --- a/pkg/code/async/vm/service.go +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 2aaba77a..00000000 --- a/pkg/code/async/vm/storage.go +++ /dev/null @@ -1,139 +0,0 @@ -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 ( - 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 { - // todo: iterate over purposes when we have more than one - purpose := storage.PurposeDeletion - - _, err := p.data.FindAnyVmStorageWithAvailableCapacity(ctx, common.CodeVmAccount.PublicKey().ToBase58(), purpose, minStorageAccountCapacity) - switch err { - case storage.ErrNotFound: - case nil: - return nil - default: - return err - } - - record, err := p.initStorageAccountOnBlockchain(ctx, common.CodeVmAccount, 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]) - - address, bump, 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: cvm.DefaultCompressedStateDepth, - AvailableCapacity: storage.GetMaxCapacity(cvm.DefaultCompressedStateDepth), - Purpose: purpose, - } - - txn := solana.NewTransaction( - common.GetSubsidizer().PublicKey().ToBytes(), - compute_budget.SetComputeUnitLimit(100_000), - compute_budget.SetComputeUnitPrice(10_000), - cvm.NewInitStorageInstruction( - &cvm.InitStorageInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), - VmStorage: address, - }, - &cvm.InitStorageInstructionArgs{ - Name: name, - VmStorageBump: bump, - }, - ), - ) - - 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) - - finalizedTxn, err := p.data.GetBlockchainTransaction(ctx, base58.Encode(sig[:]), solana.CommitmentFinalized) - if err == nil && finalizedTxn.Err == nil && finalizedTxn.Meta.Err == nil { - return record, nil - } - } - - return nil, errors.New("txn did not finalize") -} diff --git a/pkg/code/balance/calculator_test.go b/pkg/code/balance/calculator_test.go index 713fae7d..f3df62e2 100644 --- a/pkg/code/balance/calculator_test.go +++ b/pkg/code/balance/calculator_test.go @@ -18,6 +18,7 @@ import ( "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/transaction" + currency_lib "github.com/code-payments/code-server/pkg/currency" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" "github.com/code-payments/code-server/pkg/testutil" ) @@ -25,13 +26,13 @@ import ( func TestDefaultCalculationMethods_NewCodeAccount(t *testing.T) { env := setupBalanceTestEnv(t) - vmAccount := testutil.NewRandomAccount(t) + vmConfig := testutil.NewRandomVmConfig(t, true) newOwnerAccount := testutil.NewRandomAccount(t) - newTokenAccount, err := newOwnerAccount.ToTimelockVault(vmAccount, common.CoreMintAccount) + newTokenAccount, err := newOwnerAccount.ToTimelockVault(vmConfig) require.NoError(t, err) data := &balanceTestData{ - vmAccount: vmAccount, + vmConfig: vmConfig, codeUsers: []*common.Account{newOwnerAccount}, } @@ -44,7 +45,7 @@ func TestDefaultCalculationMethods_NewCodeAccount(t *testing.T) { require.NoError(t, err) assert.EqualValues(t, 0, balance) - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) require.NoError(t, err) require.Len(t, balanceByAccount, 1) assert.EqualValues(t, 0, balanceByAccount[newTokenAccount.PublicKey().ToBase58()]) @@ -58,15 +59,15 @@ func TestDefaultCalculationMethods_NewCodeAccount(t *testing.T) { func TestDefaultCalculationMethods_DepositFromExternalWallet(t *testing.T) { env := setupBalanceTestEnv(t) - vmAccount := testutil.NewRandomAccount(t) + vmConfig := testutil.NewRandomVmConfig(t, true) owner := testutil.NewRandomAccount(t) - depositAccount, err := owner.ToTimelockVault(vmAccount, common.CoreMintAccount) + depositAccount, err := owner.ToTimelockVault(vmConfig) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) data := &balanceTestData{ - vmAccount: vmAccount, + vmConfig: vmConfig, codeUsers: []*common.Account{owner}, transactions: []balanceTestTransaction{ // The following entries are added to the balance @@ -87,7 +88,7 @@ func TestDefaultCalculationMethods_DepositFromExternalWallet(t *testing.T) { accountRecords, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner) require.NoError(t, err) - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) require.NoError(t, err) require.Len(t, balanceByAccount, 1) assert.EqualValues(t, 11, balanceByAccount[depositAccount.PublicKey().ToBase58()]) @@ -101,28 +102,28 @@ func TestDefaultCalculationMethods_DepositFromExternalWallet(t *testing.T) { func TestDefaultCalculationMethods_MultipleIntents(t *testing.T) { env := setupBalanceTestEnv(t) - vmAccount := testutil.NewRandomAccount(t) + vmConfig := testutil.NewRandomVmConfig(t, true) owner1 := testutil.NewRandomAccount(t) - a1, err := owner1.ToTimelockVault(vmAccount, common.CoreMintAccount) + a1, err := owner1.ToTimelockVault(vmConfig) require.NoError(t, err) owner2 := testutil.NewRandomAccount(t) - a2, err := owner2.ToTimelockVault(vmAccount, common.CoreMintAccount) + a2, err := owner2.ToTimelockVault(vmConfig) require.NoError(t, err) owner3 := testutil.NewRandomAccount(t) - a3, err := owner3.ToTimelockVault(vmAccount, common.CoreMintAccount) + a3, err := owner3.ToTimelockVault(vmConfig) require.NoError(t, err) owner4 := testutil.NewRandomAccount(t) - a4, err := owner4.ToTimelockVault(vmAccount, common.CoreMintAccount) + a4, err := owner4.ToTimelockVault(vmConfig) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) data := &balanceTestData{ - vmAccount: vmAccount, + vmConfig: vmConfig, codeUsers: []*common.Account{owner1, owner2, owner3, owner4}, transactions: []balanceTestTransaction{ // Fund account a1 through a4 with an external deposit @@ -181,7 +182,7 @@ func TestDefaultCalculationMethods_MultipleIntents(t *testing.T) { accountRecords4, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner4) require.NoError(t, err) - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords1[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0], accountRecords2[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0], accountRecords3[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0], accountRecords4[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords1[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0], accountRecords2[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0], accountRecords3[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0], accountRecords4[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) require.NoError(t, err) require.Len(t, balanceByAccount, 4) assert.EqualValues(t, 11, balanceByAccount[a1.PublicKey().ToBase58()]) @@ -201,20 +202,20 @@ func TestDefaultCalculationMethods_MultipleIntents(t *testing.T) { func TestDefaultCalculationMethods_BackAndForth(t *testing.T) { env := setupBalanceTestEnv(t) - vmAccount := testutil.NewRandomAccount(t) + vmConfig := testutil.NewRandomVmConfig(t, true) owner1 := testutil.NewRandomAccount(t) - a1, err := owner1.ToTimelockVault(vmAccount, common.CoreMintAccount) + a1, err := owner1.ToTimelockVault(vmConfig) require.NoError(t, err) owner2 := testutil.NewRandomAccount(t) - a2, err := owner2.ToTimelockVault(vmAccount, common.CoreMintAccount) + a2, err := owner2.ToTimelockVault(vmConfig) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) data := &balanceTestData{ - vmAccount: vmAccount, + vmConfig: vmConfig, codeUsers: []*common.Account{owner1, owner2}, transactions: []balanceTestTransaction{ // Fund account a1 through an external deposit @@ -244,7 +245,7 @@ func TestDefaultCalculationMethods_BackAndForth(t *testing.T) { accountRecords2, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, owner2) require.NoError(t, err) - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords1[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0], accountRecords2[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords1[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0], accountRecords2[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) require.NoError(t, err) require.Len(t, balanceByAccount, 2) assert.EqualValues(t, 0, balanceByAccount[a1.PublicKey().ToBase58()]) @@ -260,15 +261,15 @@ func TestDefaultCalculationMethods_BackAndForth(t *testing.T) { func TestDefaultCalculationMethods_SelfPayments(t *testing.T) { env := setupBalanceTestEnv(t) - vmAccount := testutil.NewRandomAccount(t) + vmConfig := testutil.NewRandomVmConfig(t, true) ownerAccount := testutil.NewRandomAccount(t) - tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, common.CoreMintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(vmConfig) require.NoError(t, err) externalAccount := testutil.NewRandomAccount(t) data := &balanceTestData{ - vmAccount: vmAccount, + vmConfig: vmConfig, codeUsers: []*common.Account{ownerAccount}, transactions: []balanceTestTransaction{ // Fund account the token account through an external deposit @@ -291,7 +292,7 @@ func TestDefaultCalculationMethods_SelfPayments(t *testing.T) { accountRecords, err := common.GetLatestTokenAccountRecordsForOwner(env.ctx, env.data, ownerAccount) require.NoError(t, err) - balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) + balanceByAccount, err := BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) require.NoError(t, err) require.Len(t, balanceByAccount, 1) assert.EqualValues(t, 1, balanceByAccount[tokenAccount.PublicKey().ToBase58()]) @@ -305,13 +306,13 @@ func TestDefaultCalculationMethods_SelfPayments(t *testing.T) { func TestDefaultCalculationMethods_NotManagedByCode(t *testing.T) { env := setupBalanceTestEnv(t) - vmAccount := testutil.NewRandomAccount(t) + vmConfig := testutil.NewRandomVmConfig(t, true) ownerAccount := testutil.NewRandomAccount(t) - tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, common.CoreMintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(vmConfig) require.NoError(t, err) data := &balanceTestData{ - vmAccount: vmAccount, + vmConfig: vmConfig, codeUsers: []*common.Account{ownerAccount}, } @@ -329,7 +330,7 @@ func TestDefaultCalculationMethods_NotManagedByCode(t *testing.T) { _, err = CalculateFromCache(env.ctx, env.data, tokenAccount) assert.Equal(t, ErrNotManagedByCode, err) - _, err = BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[common.CoreMintAccount.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) + _, err = BatchCalculateFromCacheWithAccountRecords(env.ctx, env.data, accountRecords[vmConfig.Mint.PublicKey().ToBase58()][commonpb.AccountType_PRIMARY][0]) assert.Equal(t, ErrNotManagedByCode, err) _, err = BatchCalculateFromCacheWithTokenAccounts(env.ctx, env.data, tokenAccount) @@ -351,7 +352,7 @@ type balanceTestEnv struct { } type balanceTestData struct { - vmAccount *common.Account + vmConfig *common.VmConfig codeUsers []*common.Account transactions []balanceTestTransaction } @@ -376,7 +377,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.CoreMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(data.vmConfig) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() timelockRecord.VaultState = timelock_token_v1.StateLocked @@ -387,7 +388,7 @@ func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestDat OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: owner.PublicKey().ToBase58(), TokenAccount: timelockRecord.VaultAddress, - MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), + MintAccount: data.vmConfig.Mint.PublicKey().ToBase58(), AccountType: commonpb.AccountType_PRIMARY, } require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountInfoRecord)) @@ -399,13 +400,14 @@ func setupBalanceTestData(t *testing.T, env balanceTestEnv, data *balanceTestDat intentRecord := &intent.Record{ IntentId: txn.intentID, IntentType: intent.SendPrivatePayment, + MintAccount: data.vmConfig.Mint.PublicKey().ToBase58(), InitiatorOwnerAccount: "owner", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ DestinationOwnerAccount: testutil.NewRandomAccount(t).PublicKey().ToBase58(), DestinationTokenAccount: txn.destination.PublicKey().ToBase58(), Quantity: txn.quantity, - ExchangeCurrency: common.CoreMintSymbol, + ExchangeCurrency: currency_lib.USD, ExchangeRate: 1.0, NativeAmount: 1.0, UsdMarketValue: 1.0, diff --git a/pkg/code/common/account.go b/pkg/code/common/account.go index 90e966e5..162d563a 100644 --- a/pkg/code/common/account.go +++ b/pkg/code/common/account.go @@ -171,12 +171,12 @@ func (a *Account) Sign(message []byte) ([]byte, error) { return signature, nil } -func (a *Account) ToTimelockVault(vm, mint *Account) (*Account, error) { +func (a *Account) ToTimelockVault(vmConfig *VmConfig) (*Account, error) { if err := a.Validate(); err != nil { return nil, errors.Wrap(err, "error validating owner account") } - timelockAccounts, err := a.GetTimelockAccounts(vm, mint) + timelockAccounts, err := a.GetTimelockAccounts(vmConfig) if err != nil { return nil, err } @@ -188,7 +188,7 @@ func (a *Account) ToAssociatedTokenAccount(mint *Account) (*Account, error) { return nil, errors.Wrap(err, "error validating owner account") } - ata, err := token.GetAssociatedAccount(a.publicKey.ToBytes(), mint.publicKey.ToBytes()) + ata, err := token.GetAssociatedAccount(a.PublicKey().ToBytes(), mint.PublicKey().ToBytes()) if err != nil { return nil, err } @@ -196,27 +196,27 @@ func (a *Account) ToAssociatedTokenAccount(mint *Account) (*Account, error) { return NewAccountFromPublicKeyBytes(ata) } -func (a *Account) ToVmDepositAssociatedTokenAccount(vm, mint *Account) (*Account, error) { +func (a *Account) ToVmDepositAta(vmConfig *VmConfig) (*Account, error) { if err := a.Validate(); err != nil { return nil, errors.Wrap(err, "error validating owner account") } - vmDepositAccounts, err := a.GetVmDepositAccounts(vm, mint) + vmDepositAccounts, err := a.GetVmDepositAccounts(vmConfig) if err != nil { return nil, err } return vmDepositAccounts.Ata, nil } -func (a *Account) GetTimelockAccounts(vm, mint *Account) (*TimelockAccounts, error) { +func (a *Account) GetTimelockAccounts(vmConfig *VmConfig) (*TimelockAccounts, error) { if err := a.Validate(); err != nil { return nil, errors.Wrap(err, "error validating owner account") } stateAddress, stateBump, err := cvm.GetVirtualTimelockAccountAddress(&cvm.GetVirtualTimelockAccountAddressArgs{ - Mint: mint.publicKey.ToBytes(), - VmAuthority: GetSubsidizer().publicKey.ToBytes(), - Owner: a.publicKey.ToBytes(), + Mint: vmConfig.Mint.PublicKey().ToBytes(), + VmAuthority: vmConfig.Authority.PublicKey().ToBytes(), + Owner: a.PublicKey().ToBytes(), LockDuration: timelock_token_v1.DefaultNumDaysLocked, }) if err != nil { @@ -233,7 +233,7 @@ func (a *Account) GetTimelockAccounts(vm, mint *Account) (*TimelockAccounts, err unlockAddress, unlockBump, err := cvm.GetVmUnlockStateAccountAddress(&cvm.GetVmUnlockStateAccountAddressArgs{ VirtualAccountOwner: a.publicKey.ToBytes(), VirtualAccount: stateAddress, - Vm: vm.publicKey.ToBytes(), + Vm: vmConfig.Vm.publicKey.ToBytes(), }) if err != nil { return nil, errors.Wrap(err, "error getting unlock address") @@ -254,7 +254,7 @@ func (a *Account) GetTimelockAccounts(vm, mint *Account) (*TimelockAccounts, err return nil, errors.Wrap(err, "invalid unlock address") } - vmDepositAccounts, err := a.GetVmDepositAccounts(vm, mint) + vmDepositAccounts, err := a.GetVmDepositAccounts(vmConfig) if err != nil { return nil, errors.Wrap(err, "error getting vm deposit accounts") } @@ -273,21 +273,21 @@ func (a *Account) GetTimelockAccounts(vm, mint *Account) (*TimelockAccounts, err VmDepositAccounts: vmDepositAccounts, - Vm: vm, - Mint: mint, + Vm: vmConfig.Vm, + Mint: vmConfig.Mint, }, nil } -func (a *Account) GetVmDepositAccounts(vm, mint *Account) (*VmDepositAccounts, error) { +func (a *Account) GetVmDepositAccounts(vmConfig *VmConfig) (*VmDepositAccounts, error) { depositPdaAddress, depositPdaBump, err := cvm.GetVmDepositAddress(&cvm.GetVmDepositAddressArgs{ Depositor: a.PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), + Vm: vmConfig.Vm.PublicKey().ToBytes(), }) if err != nil { return nil, errors.Wrap(err, "error getting deposit pda address") } - depositAtaAddress, err := token.GetAssociatedAccount(depositPdaAddress, mint.PublicKey().ToBytes()) + depositAtaAddress, err := token.GetAssociatedAccount(depositPdaAddress, vmConfig.Mint.PublicKey().ToBytes()) if err != nil { return nil, errors.Wrap(err, "error getting deposit ata address") } @@ -310,13 +310,13 @@ func (a *Account) GetVmDepositAccounts(vm, mint *Account) (*VmDepositAccounts, e VaultOwner: a, - Vm: vm, - Mint: mint, + Vm: vmConfig.Vm, + Mint: vmConfig.Mint, }, nil } func (a *Account) IsManagedByCode(ctx context.Context, data code_data.Provider) (bool, error) { - timelockRecord, err := data.GetTimelockByVault(ctx, a.publicKey.ToBase58()) + timelockRecord, err := data.GetTimelockByVault(ctx, a.PublicKey().ToBase58()) if err == timelock.ErrTimelockNotFound { return false, nil } else if err != nil { @@ -327,7 +327,7 @@ func (a *Account) IsManagedByCode(ctx context.Context, data code_data.Provider) } func (a *Account) IsOnCurve() bool { - return isOnCurve(a.publicKey.ToBytes()) + return isOnCurve(a.PublicKey().ToBytes()) } func (a *Account) Validate() error { @@ -335,11 +335,11 @@ func (a *Account) Validate() error { return errors.New("account is nil") } - if err := a.publicKey.Validate(); err != nil { + if err := a.PublicKey().Validate(); err != nil { return errors.Wrap(err, "error validating public key") } - if !a.publicKey.IsPublic() { + if !a.PublicKey().IsPublic() { return errors.New("public key isn't public") } @@ -357,7 +357,7 @@ func (a *Account) Validate() error { } expectedPublicKey := ed25519.PrivateKey(a.privateKey.ToBytes()).Public().(ed25519.PublicKey) - if !bytes.Equal(a.publicKey.ToBytes(), expectedPublicKey) { + if !bytes.Equal(a.PublicKey().ToBytes(), expectedPublicKey) { return errors.New("private key doesn't map to public key") } @@ -365,7 +365,7 @@ func (a *Account) Validate() error { } func (a *Account) String() string { - return a.publicKey.ToBase58() + return a.PublicKey().ToBase58() } func (r *AccountRecords) IsManagedByCode(ctx context.Context) bool { @@ -391,15 +391,15 @@ 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{ - Address: a.State.publicKey.ToBase58(), + Address: a.State.PublicKey().ToBase58(), Bump: a.StateBump, - VaultAddress: a.Vault.publicKey.ToBase58(), + VaultAddress: a.Vault.PublicKey().ToBase58(), VaultBump: a.VaultBump, - VaultOwner: a.VaultOwner.publicKey.ToBase58(), + VaultOwner: a.VaultOwner.PublicKey().ToBase58(), VaultState: timelock_token_v1.StateUnknown, - DepositPdaAddress: a.VmDepositAccounts.Pda.publicKey.ToBase58(), + DepositPdaAddress: a.VmDepositAccounts.Pda.PublicKey().ToBase58(), DepositPdaBump: a.VmDepositAccounts.PdaBump, UnlockAt: nil, @@ -411,14 +411,14 @@ func (a *TimelockAccounts) ToDBRecord() *timelock.Record { // GetDBRecord fetches the equivalent timelock.Record for a TimelockAccounts from // the DB func (a *TimelockAccounts) GetDBRecord(ctx context.Context, data code_data.Provider) (*timelock.Record, error) { - return data.GetTimelockByVault(ctx, a.Vault.publicKey.ToBase58()) + 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) { +// GetInitializeInstruction gets a SystemTimelockInitInstruction instruction for a Timelock account +func (a *TimelockAccounts) GetInitializeInstruction(vmAuthority, memory *Account, accountIndex uint16) (solana.Instruction, error) { return cvm.NewInitTimelockInstruction( &cvm.InitTimelockInstructionAccounts{ - VmAuthority: GetSubsidizer().publicKey.ToBytes(), + VmAuthority: vmAuthority.PublicKey().ToBytes(), Vm: a.Vm.PublicKey().ToBytes(), VmMemory: memory.PublicKey().ToBytes(), VirtualAccountOwner: a.VaultOwner.PublicKey().ToBytes(), @@ -434,15 +434,15 @@ func (a *TimelockAccounts) GetInitializeInstruction(memory *Account, accountInde // 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) + _, err := data.GetBlockchainTokenAccountInfo(ctx, tokenAccount.PublicKey().ToBase58(), solana.CommitmentFinalized) switch err { case nil: // Double check there were no race conditions between other SubmitIntent // calls and scheduling. This would be highly unlikely to occur, but is a // safety precaution. - _, err := data.GetAccountInfoByTokenAddress(ctx, tokenAccount.publicKey.ToBase58()) + _, err := data.GetAccountInfoByTokenAddress(ctx, tokenAccount.PublicKey().ToBase58()) if err == nil { - return false, fmt.Sprintf("%s is not an external account", tokenAccount.publicKey.ToBase58()), nil + return false, fmt.Sprintf("%s is not an external account", tokenAccount.PublicKey().ToBase58()), nil } else if err == account.ErrAccountInfoNotFound { return true, "", nil } else if err != nil { @@ -450,9 +450,9 @@ func ValidateExternalTokenAccount(ctx context.Context, data code_data.Provider, } return true, "", nil case solana.ErrNoAccountInfo, token.ErrAccountNotFound: - return false, fmt.Sprintf("%s doesn't exist on the blockchain", tokenAccount.publicKey.ToBase58()), nil + return false, fmt.Sprintf("%s doesn't exist on the blockchain", tokenAccount.PublicKey().ToBase58()), nil case token.ErrInvalidTokenAccount: - return false, fmt.Sprintf("%s is not a core mint account", tokenAccount.publicKey.ToBase58()), nil + return false, fmt.Sprintf("%s is not a core mint account", tokenAccount.PublicKey().ToBase58()), nil default: // Unfortunate if Solana is down, but this only impacts withdraw flows, // and we need to guarantee this isn't going to something that's not diff --git a/pkg/code/common/account_test.go b/pkg/code/common/account_test.go index ca858916..ec2d8410 100644 --- a/pkg/code/common/account_test.go +++ b/pkg/code/common/account_test.go @@ -103,14 +103,13 @@ func TestInvalidAccount(t *testing.T) { } func TestConvertToTimelockVault(t *testing.T) { - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) + vmConfig := newRandomVmConfig(t) + ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) stateAddress, _, err := cvm.GetVirtualTimelockAccountAddress(&cvm.GetVirtualTimelockAccountAddressArgs{ - Mint: mintAccount.PublicKey().ToBytes(), - VmAuthority: subsidizerAccount.PublicKey().ToBytes(), + Mint: vmConfig.Mint.PublicKey().ToBytes(), + VmAuthority: vmConfig.Authority.PublicKey().ToBytes(), Owner: ownerAccount.PublicKey().ToBytes(), LockDuration: timelock_token_v1.DefaultNumDaysLocked, }) @@ -121,20 +120,19 @@ func TestConvertToTimelockVault(t *testing.T) { }) require.NoError(t, err) - tokenAccount, err := ownerAccount.ToTimelockVault(vmAccount, mintAccount) + tokenAccount, err := ownerAccount.ToTimelockVault(vmConfig) require.NoError(t, err) assert.EqualValues(t, expectedVaultAddress, tokenAccount.PublicKey().ToBytes()) } func TestGetTimelockAccounts(t *testing.T) { - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) + vmConfig := newRandomVmConfig(t) + ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) expectedStateAddress, expectedStateBump, err := cvm.GetVirtualTimelockAccountAddress(&cvm.GetVirtualTimelockAccountAddressArgs{ - Mint: mintAccount.PublicKey().ToBytes(), - VmAuthority: subsidizerAccount.PublicKey().ToBytes(), + Mint: vmConfig.Mint.PublicKey().ToBytes(), + VmAuthority: vmConfig.Authority.PublicKey().ToBytes(), Owner: ownerAccount.PublicKey().ToBytes(), LockDuration: timelock_token_v1.DefaultNumDaysLocked, }) @@ -148,11 +146,11 @@ func TestGetTimelockAccounts(t *testing.T) { expectedUnlockAddress, expectedUnlockBump, err := cvm.GetVmUnlockStateAccountAddress(&cvm.GetVmUnlockStateAccountAddressArgs{ VirtualAccountOwner: ownerAccount.PublicKey().ToBytes(), VirtualAccount: expectedStateAddress, - Vm: vmAccount.PublicKey().ToBytes(), + Vm: vmConfig.Vm.PublicKey().ToBytes(), }) require.NoError(t, err) - actual, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) + actual, err := ownerAccount.GetTimelockAccounts(vmConfig) require.NoError(t, err) assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), actual.VaultOwner.PublicKey().ToBytes()) assert.EqualValues(t, expectedStateAddress, actual.State.PublicKey().ToBytes()) @@ -161,32 +159,32 @@ func TestGetTimelockAccounts(t *testing.T) { assert.Equal(t, expectedVaultBump, actual.VaultBump) assert.EqualValues(t, expectedUnlockAddress, actual.Unlock.PublicKey().ToBytes()) assert.Equal(t, expectedUnlockBump, actual.UnlockBump) - assert.EqualValues(t, vmAccount.PublicKey().ToBytes(), actual.Vm.PublicKey().ToBytes()) - assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), actual.Mint.PublicKey().ToBytes()) + assert.EqualValues(t, vmConfig.Vm.PublicKey().ToBytes(), actual.Vm.PublicKey().ToBytes()) + assert.EqualValues(t, vmConfig.Mint.PublicKey().ToBytes(), actual.Mint.PublicKey().ToBytes()) } func TestGetVmDepositAccounts(t *testing.T) { - vmAccount := newRandomTestAccount(t) + vmConfig := newRandomVmConfig(t) + ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) expectedDepositPdaAddress, expectedDepositPdaBump, err := cvm.GetVmDepositAddress(&cvm.GetVmDepositAddressArgs{ Depositor: ownerAccount.PublicKey().ToBytes(), - Vm: vmAccount.PublicKey().ToBytes(), + Vm: vmConfig.Vm.PublicKey().ToBytes(), }) require.NoError(t, err) - expectedDepositAtaAddress, err := token.GetAssociatedAccount(expectedDepositPdaAddress, mintAccount.PublicKey().ToBytes()) + expectedDepositAtaAddress, err := token.GetAssociatedAccount(expectedDepositPdaAddress, vmConfig.Mint.PublicKey().ToBytes()) require.NoError(t, err) - actual, err := ownerAccount.GetVmDepositAccounts(vmAccount, mintAccount) + actual, err := ownerAccount.GetVmDepositAccounts(vmConfig) require.NoError(t, err) assert.EqualValues(t, ownerAccount.PublicKey().ToBytes(), actual.VaultOwner.PublicKey().ToBytes()) assert.EqualValues(t, expectedDepositPdaAddress, actual.Pda.PublicKey().ToBytes()) assert.Equal(t, expectedDepositPdaBump, actual.PdaBump) assert.EqualValues(t, expectedDepositAtaAddress, actual.Ata.PublicKey().ToBytes()) - assert.EqualValues(t, vmAccount.PublicKey().ToBytes(), actual.Vm.PublicKey().ToBytes()) - assert.EqualValues(t, mintAccount.PublicKey().ToBytes(), actual.Mint.PublicKey().ToBytes()) + assert.EqualValues(t, vmConfig.Vm.PublicKey().ToBytes(), actual.Vm.PublicKey().ToBytes()) + assert.EqualValues(t, vmConfig.Mint.PublicKey().ToBytes(), actual.Mint.PublicKey().ToBytes()) } func TestIsOnCurve(t *testing.T) { @@ -208,11 +206,11 @@ func TestIsAccountManagedByCode_TimelockState(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() - vmAccount := newRandomTestAccount(t) + vmConfig := newRandomVmConfig(t) + ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmConfig) require.NoError(t, err) // No record of the account anywhere @@ -259,11 +257,11 @@ func TestIsAccountManagedByCode_OtherAccounts(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() - vmAccount := newRandomTestAccount(t) + vmConfig := newRandomVmConfig(t) + ownerAccount := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) - timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmAccount, mintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmConfig) require.NoError(t, err) require.NoError(t, data.SaveTimelock(ctx, timelockAccounts.ToDBRecord())) diff --git a/pkg/code/common/mint.go b/pkg/code/common/mint.go index 031ac4fa..d12bf991 100644 --- a/pkg/code/common/mint.go +++ b/pkg/code/common/mint.go @@ -1,13 +1,11 @@ package common import ( - "fmt" - "strconv" - "strings" - - "github.com/pkg/errors" + "bytes" + commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" "github.com/code-payments/code-server/pkg/code/config" + "github.com/code-payments/code-server/pkg/solana/currencycreator" "github.com/code-payments/code-server/pkg/usdc" "github.com/code-payments/code-server/pkg/usdt" ) @@ -18,8 +16,18 @@ var ( CoreMintDecimals = config.CoreMintDecimals CoreMintName = config.CoreMintName CoreMintSymbol = config.CoreMintSymbol + + jeffyMintAccount, _ = NewAccountFromPublicKeyString(config.JeffyMintPublicKey) ) +func GetBackwardsCompatMint(protoMint *commonpb.SolanaAccountId) (*Account, error) { + if protoMint == nil { + return CoreMintAccount, nil + } + return NewAccountFromProto(protoMint) + +} + func FromCoreMintQuarks(quarks uint64) uint64 { return quarks / CoreMintQuarksPerUnit } @@ -28,36 +36,8 @@ 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 +func IsCoreMint(mint *Account) bool { + return bytes.Equal(mint.PublicKey().ToBytes(), CoreMintAccount.PublicKey().ToBytes()) } func IsCoreMintUsdStableCoin() bool { @@ -68,3 +48,10 @@ func IsCoreMintUsdStableCoin() bool { return false } } + +func GetMintQuarksPerUnit(mint *Account) uint64 { + if mint.PublicKey().ToBase58() == CoreMintAccount.PublicKey().ToBase58() { + return CoreMintQuarksPerUnit + } + return currencycreator.DefaultMintQuarksPerUnit +} diff --git a/pkg/code/common/mint_test.go b/pkg/code/common/mint_test.go deleted file mode 100644 index 31cc11e3..00000000 --- a/pkg/code/common/mint_test.go +++ /dev/null @@ -1,37 +0,0 @@ -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_test.go b/pkg/code/common/owner_test.go index 7576ff8e..7ec1e150 100644 --- a/pkg/code/common/owner_test.go +++ b/pkg/code/common/owner_test.go @@ -18,19 +18,19 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) + coreVmConfig := newRandomVmConfig(t) + + swapMintAccount := newRandomTestAccount(t) + owner := newRandomTestAccount(t) swapAuthority := newRandomTestAccount(t) - coreMintAccount := newRandomTestAccount(t) - swapMintAccount := newRandomTestAccount(t) _, err := GetOwnerMetadata(ctx, data, owner) assert.Equal(t, ErrOwnerNotFound, err) // Later calls intent to OpenAccounts - timelockAccounts, err := owner.GetTimelockAccounts(vmAccount, coreMintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(coreVmConfig) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -40,7 +40,7 @@ func TestGetOwnerMetadata_User12Words(t *testing.T) { OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: timelockRecord.VaultOwner, TokenAccount: timelockRecord.VaultAddress, - MintAccount: coreMintAccount.PublicKey().ToBase58(), + MintAccount: coreVmConfig.Mint.PublicKey().ToBase58(), AccountType: commonpb.AccountType_PRIMARY, } require.NoError(t, data.CreateAccountInfo(ctx, primaryAccountInfoRecord)) @@ -87,15 +87,14 @@ func TestGetOwnerMetadata_RemoteSendGiftCard(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() - vmAccount := newRandomTestAccount(t) - subsidizerAccount = newRandomTestAccount(t) + coreVmConfig := newRandomVmConfig(t) + owner := newRandomTestAccount(t) - mintAccount := newRandomTestAccount(t) _, err := GetOwnerMetadata(ctx, data, owner) assert.Equal(t, ErrOwnerNotFound, err) - timelockAccounts, err := owner.GetTimelockAccounts(vmAccount, mintAccount) + timelockAccounts, err := owner.GetTimelockAccounts(coreVmConfig) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -105,7 +104,7 @@ func TestGetOwnerMetadata_RemoteSendGiftCard(t *testing.T) { OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: timelockRecord.VaultOwner, TokenAccount: timelockRecord.VaultAddress, - MintAccount: mintAccount.PublicKey().ToBase58(), + MintAccount: coreVmConfig.Mint.PublicKey().ToBase58(), AccountType: commonpb.AccountType_REMOTE_SEND_GIFT_CARD, } require.NoError(t, data.CreateAccountInfo(ctx, accountInfoRecord)) @@ -121,12 +120,13 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { ctx := context.Background() data := code_data.NewTestDataProvider() - subsidizerAccount = newRandomTestAccount(t) - owner := newRandomTestAccount(t) - coreMintAccount := newRandomTestAccount(t) - jeffyMintAccount := newRandomTestAccount(t) + coreVmConfig := newRandomVmConfig(t) + jeffyVmConfig := newRandomVmConfig(t) + swapMintAccount := newRandomTestAccount(t) + owner := newRandomTestAccount(t) + actual, err := GetLatestTokenAccountRecordsForOwner(ctx, data, owner) require.NoError(t, err) assert.Empty(t, actual) @@ -142,8 +142,8 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { }{ {authority1, commonpb.AccountType_PRIMARY}, } { - for _, mint := range []*Account{coreMintAccount, jeffyMintAccount} { - timelockAccounts, err := authorityAndType.account.GetTimelockAccounts(newRandomTestAccount(t), mint) + for _, vmConfig := range []*VmConfig{coreVmConfig, jeffyVmConfig} { + timelockAccounts, err := authorityAndType.account.GetTimelockAccounts(vmConfig) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -153,7 +153,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: timelockRecord.VaultOwner, TokenAccount: timelockRecord.VaultAddress, - MintAccount: mint.PublicKey().ToBase58(), + MintAccount: vmConfig.Mint.PublicKey().ToBase58(), AccountType: authorityAndType.accountType, } require.NoError(t, data.CreateAccountInfo(ctx, accountInfoRecord)) @@ -164,7 +164,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { authority2, authority3, } { - timelockAccounts, err := authority.GetTimelockAccounts(newRandomTestAccount(t), coreMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(coreVmConfig) require.NoError(t, err) timelockRecord := timelockAccounts.ToDBRecord() @@ -174,7 +174,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { OwnerAccount: owner.PublicKey().ToBase58(), AuthorityAccount: timelockRecord.VaultOwner, TokenAccount: timelockRecord.VaultAddress, - MintAccount: coreMintAccount.PublicKey().ToBase58(), + MintAccount: coreVmConfig.Mint.PublicKey().ToBase58(), AccountType: commonpb.AccountType_POOL, Index: uint64(i), } @@ -196,11 +196,11 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { require.NoError(t, err) require.Len(t, actual, 3) - coreMintRecords, ok := actual[coreMintAccount.PublicKey().ToBase58()] + coreMintRecords, ok := actual[coreVmConfig.Mint.PublicKey().ToBase58()] require.True(t, ok) require.Len(t, coreMintRecords, 2) - jeffyMintRecords, ok := actual[jeffyMintAccount.PublicKey().ToBase58()] + jeffyMintRecords, ok := actual[jeffyVmConfig.Mint.PublicKey().ToBase58()] require.True(t, ok) require.Len(t, jeffyMintRecords, 1) @@ -215,7 +215,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { assert.Equal(t, records[0].General.AccountType, commonpb.AccountType_PRIMARY) assert.Equal(t, records[0].Timelock.VaultOwner, authority1.PublicKey().ToBase58()) assert.Equal(t, records[0].General.TokenAccount, records[0].Timelock.VaultAddress) - assert.Equal(t, records[0].General.MintAccount, coreMintAccount.PublicKey().ToBase58()) + assert.Equal(t, records[0].General.MintAccount, coreVmConfig.Mint.PublicKey().ToBase58()) records, ok = coreMintRecords[commonpb.AccountType_POOL] require.True(t, ok) @@ -225,14 +225,14 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { assert.Equal(t, records[0].General.AccountType, commonpb.AccountType_POOL) assert.Equal(t, records[0].Timelock.VaultOwner, authority2.PublicKey().ToBase58()) assert.Equal(t, records[0].General.TokenAccount, records[0].Timelock.VaultAddress) - assert.Equal(t, records[0].General.MintAccount, coreMintAccount.PublicKey().ToBase58()) + assert.Equal(t, records[0].General.MintAccount, coreVmConfig.Mint.PublicKey().ToBase58()) assert.EqualValues(t, records[0].General.Index, 0) assert.Equal(t, records[1].General.AuthorityAccount, authority3.PublicKey().ToBase58()) assert.Equal(t, records[1].General.AccountType, commonpb.AccountType_POOL) assert.Equal(t, records[1].Timelock.VaultOwner, authority3.PublicKey().ToBase58()) assert.Equal(t, records[1].General.TokenAccount, records[1].Timelock.VaultAddress) - assert.Equal(t, records[1].General.MintAccount, coreMintAccount.PublicKey().ToBase58()) + assert.Equal(t, records[1].General.MintAccount, coreVmConfig.Mint.PublicKey().ToBase58()) assert.EqualValues(t, records[1].General.Index, 1) records, ok = jeffyMintRecords[commonpb.AccountType_PRIMARY] @@ -242,7 +242,7 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) { assert.Equal(t, records[0].General.AccountType, commonpb.AccountType_PRIMARY) assert.Equal(t, records[0].Timelock.VaultOwner, authority1.PublicKey().ToBase58()) assert.Equal(t, records[0].General.TokenAccount, records[0].Timelock.VaultAddress) - assert.Equal(t, records[0].General.MintAccount, jeffyMintAccount.PublicKey().ToBase58()) + assert.Equal(t, records[0].General.MintAccount, jeffyVmConfig.Mint.PublicKey().ToBase58()) records, ok = swapMintRecords[commonpb.AccountType_SWAP] require.True(t, ok) diff --git a/pkg/code/common/testutil.go b/pkg/code/common/testutil.go index b7949573..9241f93f 100644 --- a/pkg/code/common/testutil.go +++ b/pkg/code/common/testutil.go @@ -12,3 +12,13 @@ func newRandomTestAccount(t *testing.T) *Account { require.NoError(t, err) return account } + +// Required because we'd have a dependency loop with the testutil package +func newRandomVmConfig(t *testing.T) *VmConfig { + return &VmConfig{ + Authority: newRandomTestAccount(t), + Vm: newRandomTestAccount(t), + Omnibus: newRandomTestAccount(t), + Mint: newRandomTestAccount(t), + } +} diff --git a/pkg/code/common/vm.go b/pkg/code/common/vm.go index 9c3a1147..9f557e9f 100644 --- a/pkg/code/common/vm.go +++ b/pkg/code/common/vm.go @@ -1,6 +1,12 @@ package common -import "github.com/code-payments/code-server/pkg/code/config" +import ( + "context" + "errors" + + "github.com/code-payments/code-server/pkg/code/config" + code_data "github.com/code-payments/code-server/pkg/code/data" +) var ( // The well-known Code VM instance @@ -8,4 +14,47 @@ var ( // The well-known Code VM instance omnibus account CodeVmOmnibusAccount, _ = NewAccountFromPublicKeyString(config.VmOmnibusPublicKey) + + // todo: DB store to track VM per mint + jeffyAuthority, _ = NewAccountFromPublicKeyString(config.JeffyAuthorityPublicKey) + jeffyVmAccount, _ = NewAccountFromPublicKeyString(config.JeffyVmAccountPublicKey) + jeffyVmOmnibusAccount, _ = NewAccountFromPublicKeyString(config.JeffyVmOmnibusPublicKey) ) + +type VmConfig struct { + Authority *Account + Vm *Account + Omnibus *Account + Mint *Account +} + +func GetVmConfigForMint(ctx context.Context, data code_data.Provider, mint *Account) (*VmConfig, error) { + switch mint.PublicKey().ToBase58() { + case CoreMintAccount.PublicKey().ToBase58(): + return &VmConfig{ + Authority: GetSubsidizer(), + Vm: CodeVmAccount, + Omnibus: CodeVmOmnibusAccount, + Mint: CoreMintAccount, + }, nil + case jeffyMintAccount.PublicKey().ToBase58(): + vaultRecord, err := data.GetKey(ctx, jeffyAuthority.PublicKey().ToBase58()) + if err != nil { + return nil, err + } + + authorityAccount, err := NewAccountFromPrivateKeyString(vaultRecord.PrivateKey) + if err != nil { + return nil, err + } + + return &VmConfig{ + Authority: authorityAccount, + Vm: jeffyVmAccount, + Omnibus: jeffyVmOmnibusAccount, + Mint: mint, + }, nil + default: + return nil, errors.New("unsupported mint") + } +} diff --git a/pkg/code/config/config.go b/pkg/code/config/config.go index c84ff4ee..8dc11349 100644 --- a/pkg/code/config/config.go +++ b/pkg/code/config/config.go @@ -3,7 +3,6 @@ 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" ) @@ -16,7 +15,7 @@ const ( CoreMintQuarksPerUnit = uint64(usdc.QuarksPerUsdc) CoreMintDecimals = usdc.Decimals CoreMintName = "USDC" - CoreMintSymbol = currency_lib.USDC + CoreMintSymbol = "USDC" // Random value. Replace with real subsidizer public keys SubsidizerPublicKey = "84ydcM4Yp59W6aZP6eSaKiAMaKidNLfb5k318sT2pm14" @@ -24,6 +23,12 @@ const ( // Random value. Replace with real VM public keys VmAccountPublicKey = "BVMGLfRgr3nVFCH5DuW6VR2kfSDxq4EFEopXfwCDpYzb" VmOmnibusPublicKey = "GNw1t85VH8b1CcwB5933KBC7PboDPJ5EcQdGynbfN1Pb" + + // todo: DB store to track VM per mint + JeffyMintPublicKey = "52MNGpgvydSwCtC2H4qeiZXZ1TxEuRVCRGa8LAfk2kSj" + JeffyAuthorityPublicKey = "jfy1btcfsjSn2WCqLVaxiEjp4zgmemGyRsdCPbPwnZV" + JeffyVmAccountPublicKey = "Bii3UFB9DzPq6UxgewF5iv9h1Gi8ZnP6mr7PtocHGNta" + JeffyVmOmnibusPublicKey = "CQ5jni8XTXEcMFXS1ytNyTVbJBZHtHCzEtjBPowB3MLD" ) var ( diff --git a/pkg/code/currency/time.go b/pkg/code/currency/time.go index 31a970e9..d2d79418 100644 --- a/pkg/code/currency/time.go +++ b/pkg/code/currency/time.go @@ -4,8 +4,10 @@ import ( "time" ) -// todo: add tests, but generally well tested in server tests since that's where most of this originated -// todo: does this belong in an exchange-specific package? +const ( + exchangeRateUpdatesPerHour = 4 + timePerExchangeRateUpdate = time.Hour / exchangeRateUpdatesPerHour +) // GetLatestExchangeRateTime gets the latest time for fetching an exchange rate. // By synchronizing on a time, we can eliminate the amount of perceived volatility @@ -15,8 +17,8 @@ func GetLatestExchangeRateTime() time.Time { // Notably, don't fall exactly on the 15 minute interval, so we remove 1 second. // The way our postgres DB query is setup, the start of UTC day is unlikely to // generate results. - secondsIn15Minutes := int64(15 * time.Minute / time.Second) + secondsInUpdateInterval := int64(timePerExchangeRateUpdate / time.Second) queryTimeUnix := time.Now().Unix() - queryTimeUnix = queryTimeUnix - (queryTimeUnix % secondsIn15Minutes) - 1 + queryTimeUnix = queryTimeUnix - (queryTimeUnix % secondsInUpdateInterval) - 1 return time.Unix(queryTimeUnix, 0) } diff --git a/pkg/code/currency/usd_market_value.go b/pkg/code/currency/usd_market_value.go new file mode 100644 index 00000000..40261c54 --- /dev/null +++ b/pkg/code/currency/usd_market_value.go @@ -0,0 +1,24 @@ +package currency + +import ( + "context" + "time" + + "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" + currency_lib "github.com/code-payments/code-server/pkg/currency" +) + +// CalculateUsdMarketValue calculates the current USD market value of a crypto +// amount in quarks. +func CalculateUsdMarketValue(ctx context.Context, data code_data.Provider, mint *common.Account, quarks uint64, at time.Time) (float64, error) { + usdExchangeRecord, err := data.GetExchangeRate(ctx, currency_lib.USD, at) + if err != nil { + return 0, err + } + + quarksPerUnit := common.GetMintQuarksPerUnit(mint) + units := float64(quarks) / float64(quarksPerUnit) + marketValue := usdExchangeRecord.Rate * units + return marketValue, nil +} diff --git a/pkg/code/currency/validation.go b/pkg/code/currency/validation.go index 75fc0dbd..69d5b5bf 100644 --- a/pkg/code/currency/validation.go +++ b/pkg/code/currency/validation.go @@ -2,9 +2,7 @@ package currency import ( "context" - "fmt" "math" - "strings" "time" "github.com/pkg/errors" @@ -16,18 +14,13 @@ import ( "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" + "github.com/code-payments/code-server/pkg/solana/currencycreator" ) -// 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. -func GetPotentialClientExchangeRates(ctx context.Context, data code_data.Provider, code currency_lib.Code) ([]*currency.ExchangeRateRecord, error) { +// GetPotentialClientCoreMintExchangeRates gets a set of fiat exchange rates that +// a client attempting to maintain a latest state could have fetched from the +// currency server for the core mint. +func GetPotentialClientCoreMintExchangeRates(ctx context.Context, data code_data.Provider, code currency_lib.Code) ([]*currency.ExchangeRateRecord, error) { exchangeRecords, err := data.GetExchangeRateHistory( ctx, code, @@ -65,46 +58,62 @@ func GetPotentialClientExchangeRates(ctx context.Context, data code_data.Provide // ValidateClientExchangeData validates proto exchange data provided by a client func ValidateClientExchangeData(ctx context.Context, data code_data.Provider, proto *transactionpb.ExchangeData) (bool, string, error) { - currencyCode := strings.ToLower(proto.Currency) - switch currencyCode { - case string(common.CoreMintSymbol): - if proto.ExchangeRate != 1.0 { - 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)) - if err != nil { + mint, err := common.GetBackwardsCompatMint(proto.Mint) + if err != nil { + return false, "", err + } + + latestExchangeRateTime := GetLatestExchangeRateTime() + + var foundRate float64 + var isClientRateValid bool + for i := range 3 { + exchangeRateTime := latestExchangeRateTime.Add(time.Duration(-i) * timePerExchangeRateUpdate) + + coreMintFiatExchangeRateRecord, err := data.GetExchangeRate(ctx, currency_lib.Code(proto.Currency), exchangeRateTime) + if err == currency.ErrNotFound { + continue + } else if err != nil { return false, "", err } - // Alternatively, we could find the highest and lowest value and ensure - // the requested rate falls in that range. However, this method allows - // us to ensure clients are getting their data from code-server. - var foundExchangeRate bool - for _, exchangeRecord := range exchangeRecords { - // Avoid issues with floating points by examining the percentage - // difference - percentDiff := math.Abs(exchangeRecord.Rate-proto.ExchangeRate) / exchangeRecord.Rate - if percentDiff < 0.001 { - foundExchangeRate = true - break + pricePerCoreMint := 1.0 + if mint.PublicKey().ToBase58() != common.CoreMintAccount.PublicKey().ToBase58() { + reserveRecord, err := data.GetCurrencyReserveAtTime(ctx, mint.PublicKey().ToBase58(), exchangeRateTime) + if err == currency.ErrNotFound { + continue + } else if err != nil { + return false, "", err } + + pricePerCoreMint, _ = currencycreator.EstimateCurrentPrice(reserveRecord.SupplyFromBonding).Float64() } - if !foundExchangeRate { - return false, "fiat exchange rate is stale", nil + actualRate := pricePerCoreMint * coreMintFiatExchangeRateRecord.Rate + + // Avoid issues with floating points by examining the percentage difference + // + // todo: configurable error tolerance? + percentDiff := math.Abs(actualRate-proto.ExchangeRate) / actualRate + if percentDiff < 0.001 { + isClientRateValid = true + foundRate = actualRate + break } } + if !isClientRateValid { + return false, "fiat exchange rate is stale or invalid", nil + } + // Validate that the native amount and exchange rate fall reasonably within - // the amount of quarks to send in the transaction. This must consider any - // truncation at the client due to minimum bucket sizes. + // the amount of quarks to send in the transaction. // - // todo: This uses string conversions, which is less than ideal, but the only - // thing available at the time of writing this for conversion. - quarksFromCurrency, _ := common.StrToQuarks(fmt.Sprintf("%.6f", proto.NativeAmount/proto.ExchangeRate)) - if math.Abs(float64(quarksFromCurrency-int64(proto.Quarks))) > float64(SmallestSendAmount) { + // todo: configurable error tolerance? + quarksPerUnit := common.GetMintQuarksPerUnit(mint) + unitsOfMint := proto.NativeAmount / foundRate + expectedQuarks := int64(unitsOfMint * float64(quarksPerUnit)) + if math.Abs(float64(expectedQuarks-int64(proto.Quarks))) > 100 { return false, "payment native amount and quark value mismatch", nil } diff --git a/pkg/code/data/intent/intent.go b/pkg/code/data/intent/intent.go index 6dce095e..d9d7321d 100644 --- a/pkg/code/data/intent/intent.go +++ b/pkg/code/data/intent/intent.go @@ -42,6 +42,8 @@ type Record struct { IntentId string IntentType Type + MintAccount string + InitiatorOwnerAccount string OpenAccountsMetadata *OpenAccountsMetadata @@ -50,8 +52,6 @@ type Record struct { ReceivePaymentsPubliclyMetadata *ReceivePaymentsPubliclyMetadata PublicDistributionMetadata *PublicDistributionMetadata - ExtendedMetadata []byte - State State Version uint64 @@ -152,6 +152,8 @@ func (r *Record) Clone() Record { IntentId: r.IntentId, IntentType: r.IntentType, + MintAccount: r.MintAccount, + InitiatorOwnerAccount: r.InitiatorOwnerAccount, OpenAccountsMetadata: openAccountsMetadata, @@ -160,8 +162,6 @@ func (r *Record) Clone() Record { ReceivePaymentsPubliclyMetadata: receivePaymentsPubliclyMetadata, PublicDistributionMetadata: publicDistributionMetadata, - ExtendedMetadata: r.ExtendedMetadata, - State: r.State, Version: r.Version, @@ -176,6 +176,8 @@ func (r *Record) CopyTo(dst *Record) { dst.IntentId = r.IntentId dst.IntentType = r.IntentType + dst.MintAccount = r.MintAccount + dst.InitiatorOwnerAccount = r.InitiatorOwnerAccount dst.OpenAccountsMetadata = r.OpenAccountsMetadata @@ -184,8 +186,6 @@ func (r *Record) CopyTo(dst *Record) { dst.ReceivePaymentsPubliclyMetadata = r.ReceivePaymentsPubliclyMetadata dst.PublicDistributionMetadata = r.PublicDistributionMetadata - dst.ExtendedMetadata = r.ExtendedMetadata - dst.State = r.State dst.Version = r.Version @@ -202,6 +202,10 @@ func (r *Record) Validate() error { return errors.New("intent type is required") } + if len(r.MintAccount) == 0 { + return errors.New("mint account is required") + } + if len(r.InitiatorOwnerAccount) == 0 { return errors.New("initiator owner account is required") } diff --git a/pkg/code/data/intent/postgres/model.go b/pkg/code/data/intent/postgres/model.go index b1c7d55f..a8eaa1e9 100644 --- a/pkg/code/data/intent/postgres/model.go +++ b/pkg/code/data/intent/postgres/model.go @@ -11,6 +11,7 @@ import ( "github.com/jmoiron/sqlx" + "github.com/code-payments/code-server/pkg/code/config" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/currency" @@ -24,28 +25,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"` - 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"` - Version int64 `db:"version"` - CreatedAt time.Time `db:"created_at"` + Id sql.NullInt64 `db:"id"` + IntentId string `db:"intent_id"` + IntentType uint `db:"intent_type"` + Mint sql.NullString `db:"mint"` + 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"` + State uint `db:"state"` + Version int64 `db:"version"` + CreatedAt time.Time `db:"created_at"` Accounts []*intentAccountModel } @@ -59,15 +59,22 @@ func toIntentModel(obj *intent.Record) (*intentModel, error) { obj.CreatedAt = time.Now().UTC() } + // For backwards compatibility + var mint sql.NullString + if obj.MintAccount != config.CoreMintPublicKeyString { + mint.Valid = true + mint.String = obj.MintAccount + } + m := &intentModel{ - 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, - Version: int64(obj.Version), + Id: sql.NullInt64{Int64: int64(obj.Id), Valid: true}, + IntentId: obj.IntentId, + IntentType: uint(obj.IntentType), + Mint: mint, + InitiatorOwner: obj.InitiatorOwnerAccount, + State: uint(obj.State), + CreatedAt: obj.CreatedAt, + Version: int64(obj.Version), } switch obj.IntentType { @@ -122,12 +129,18 @@ 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), Version: uint64(obj.Version), CreatedAt: obj.CreatedAt.UTC(), } + // For backwards compatibility + if obj.Mint.Valid { + record.MintAccount = obj.Mint.String + } else { + record.MintAccount = config.CoreMintPublicKeyString + } + switch record.IntentType { case intent.OpenAccounts: record.OpenAccountsMetadata = &intent.OpenAccountsMetadata{} @@ -186,22 +199,23 @@ 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, 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, version, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20 + 1, $21) + (intent_id, intent_type, mint, 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, state, version, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19 + 1, $20) ON CONFLICT (intent_id) DO UPDATE - SET state = $19, version = ` + intentTableName + `.version + 1 - WHERE ` + intentTableName + `.intent_id = $1 AND ` + intentTableName + `.version = $20 + SET state = $18, version = ` + intentTableName + `.version + 1 + WHERE ` + intentTableName + `.intent_id = $1 AND ` + intentTableName + `.version = $19 RETURNING - 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, version, created_at` + id, intent_id, intent_type, mint, 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, state, version, created_at` err := tx.QueryRowxContext( ctx, query, m.IntentId, m.IntentType, + m.Mint, m.InitiatorOwner, m.Source, m.DestinationOwnerAccount, @@ -216,8 +230,6 @@ func (m *intentModel) dbSave(ctx context.Context, db *sqlx.DB) error { m.IsRemoteSend, m.IsReturned, m.IsIssuerVoidingGiftCard, - m.IsMicroPayment, - m.ExtendedMetadata, m.State, m.Version, m.CreatedAt, @@ -343,7 +355,7 @@ func dbGetAccounts(ctx context.Context, db *sqlx.DB, intentType intent.Type, pag func dbGetIntentByIntentID(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, 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, version, created_at + query := `SELECT id, intent_id, intent_type, mint, 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, state, version, created_at FROM ` + intentTableName + ` WHERE intent_id = $1 LIMIT 1` @@ -363,7 +375,7 @@ func dbGetIntentByIntentID(ctx context.Context, db *sqlx.DB, intentID string) (* func dbGetIntentByID(ctx context.Context, db *sqlx.DB, id int64) (*intentModel, error) { res := &intentModel{} - 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, version, created_at + query := `SELECT id, intent_id, intent_type, mint, 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, state, version, created_at FROM ` + intentTableName + ` WHERE id = $1 LIMIT 1` @@ -386,7 +398,7 @@ func dbGetAllByOwner(ctx context.Context, db *sqlx.DB, owner string, cursor q.Cu models := []*intentModel{} opts := []any{owner} - query1 := `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, version, created_at + query1 := `SELECT id, intent_id, intent_type, mint, 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, state, version, created_at FROM ` + intentTableName + ` WHERE (owner = $1 OR destination_owner = $1) ` @@ -455,7 +467,7 @@ func dbGetAllByOwner(ctx context.Context, db *sqlx.DB, owner string, cursor q.Cu 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, 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, version, created_at + query := `SELECT id, intent_id, intent_type, mint, 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, state, version, created_at FROM ` + intentTableName + ` WHERE destination = $1 and intent_type = $2 AND state != $3 AND is_remote_send IS TRUE LIMIT 2 @@ -487,7 +499,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, 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, version, created_at + query := `SELECT id, intent_id, intent_type, mint, 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, state, version, 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_test.go b/pkg/code/data/intent/postgres/store_test.go index 90579eac..e44d39ac 100644 --- a/pkg/code/data/intent/postgres/store_test.go +++ b/pkg/code/data/intent/postgres/store_test.go @@ -25,26 +25,25 @@ const ( intent_id TEXT NOT NULL UNIQUE, intent_type INTEGER NOT NULL, - owner text NOT NULL, + mint TEXT NULL, + + owner TEXT NOT NULL, source TEXT NULL, destination TEXT NULL, destination_owner TEXT NULL, quantity BIGINT NULL CHECK (quantity >= 0), - exchange_currency varchar(3) NULL, - exchange_rate numeric(18, 9) NULL, - native_amount numeric(18, 9) NULL, - usd_market_value numeric(18, 9) NULL, + exchange_currency VARCHAR(3) NULL, + exchange_rate NUMERIC(18, 9) NULL, + native_amount NUMERIC(18, 9) NULL, + usd_market_value NUMERIC(18, 9) NULL, is_withdraw BOOL NOT NULL, is_deposit BOOL NOT NULL, is_remote_send BOOL NOT NULL, is_returned BOOL NOT NULL, is_issuer_voiding_gift_card BOOL NOT NULL, - is_micro_payment BOOL NOT NULL, - - extended_metadata BYTEA NULL, state INTEGER NOT NULL, diff --git a/pkg/code/data/intent/tests/tests.go b/pkg/code/data/intent/tests/tests.go index 035ab17a..bfa8432f 100644 --- a/pkg/code/data/intent/tests/tests.go +++ b/pkg/code/data/intent/tests/tests.go @@ -44,9 +44,9 @@ func testOpenAccountsRoundTrip(t *testing.T, s intent.Store) { expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.OpenAccounts, + MintAccount: "test_mint", InitiatorOwnerAccount: "test_owner", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, - ExtendedMetadata: []byte("extended_metadata"), State: intent.StateUnknown, CreatedAt: time.Now(), } @@ -60,9 +60,9 @@ func testOpenAccountsRoundTrip(t *testing.T, s intent.Store) { require.NoError(t, err) assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) + assert.Equal(t, cloned.MintAccount, actual.MintAccount) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) require.NotNil(t, actual.OpenAccountsMetadata) - 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) @@ -82,15 +82,15 @@ func testExternalDepositRoundTrip(t *testing.T, s intent.Store) { expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.ExternalDeposit, + MintAccount: "test_mint", InitiatorOwnerAccount: "test_owner", ExternalDepositMetadata: &intent.ExternalDepositMetadata{ DestinationTokenAccount: "test_destination_token", Quantity: 12345, UsdMarketValue: 1.2345, }, - ExtendedMetadata: []byte("extended_metadata"), - State: intent.StateUnknown, - CreatedAt: time.Now(), + State: intent.StateUnknown, + CreatedAt: time.Now(), } cloned := expected.Clone() err = s.Save(ctx, &expected) @@ -102,12 +102,12 @@ func testExternalDepositRoundTrip(t *testing.T, s intent.Store) { require.NoError(t, err) assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) + assert.Equal(t, cloned.MintAccount, actual.MintAccount) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) require.NotNil(t, actual.ExternalDepositMetadata) 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) @@ -127,6 +127,7 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.SendPublicPayment, + MintAccount: "test_mint", InitiatorOwnerAccount: "test_owner", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ DestinationOwnerAccount: "test_destination_owner", @@ -141,9 +142,8 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { IsWithdrawal: true, IsRemoteSend: true, }, - ExtendedMetadata: []byte("extended_metadata"), - State: intent.StateUnknown, - CreatedAt: time.Now(), + State: intent.StateUnknown, + CreatedAt: time.Now(), } cloned := expected.Clone() err = s.Save(ctx, &expected) @@ -155,6 +155,7 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { require.NoError(t, err) assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) + assert.Equal(t, cloned.MintAccount, actual.MintAccount) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) require.NotNil(t, actual.SendPublicPaymentMetadata) assert.Equal(t, cloned.SendPublicPaymentMetadata.DestinationOwnerAccount, actual.SendPublicPaymentMetadata.DestinationOwnerAccount) @@ -166,7 +167,6 @@ func testSendPublicPaymentRoundTrip(t *testing.T, s intent.Store) { assert.Equal(t, cloned.SendPublicPaymentMetadata.UsdMarketValue, actual.SendPublicPaymentMetadata.UsdMarketValue) assert.Equal(t, cloned.SendPublicPaymentMetadata.IsWithdrawal, actual.SendPublicPaymentMetadata.IsWithdrawal) assert.Equal(t, cloned.SendPublicPaymentMetadata.IsRemoteSend, actual.SendPublicPaymentMetadata.IsRemoteSend) - 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) @@ -186,6 +186,7 @@ func testReceivePaymentsPubliclyRoundTrip(t *testing.T, s intent.Store) { expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.ReceivePaymentsPublicly, + MintAccount: "test_mint", InitiatorOwnerAccount: "test_owner", ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{ Source: "test_source", @@ -198,9 +199,8 @@ func testReceivePaymentsPubliclyRoundTrip(t *testing.T, s intent.Store) { OriginalNativeAmount: 1234.5, UsdMarketValue: 999.99, }, - ExtendedMetadata: []byte("extended_metadata"), - State: intent.StateUnknown, - CreatedAt: time.Now(), + State: intent.StateUnknown, + CreatedAt: time.Now(), } cloned := expected.Clone() err = s.Save(ctx, &expected) @@ -212,6 +212,7 @@ func testReceivePaymentsPubliclyRoundTrip(t *testing.T, s intent.Store) { require.NoError(t, err) assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) + assert.Equal(t, cloned.MintAccount, actual.MintAccount) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) require.NotNil(t, actual.ReceivePaymentsPubliclyMetadata) assert.Equal(t, cloned.ReceivePaymentsPubliclyMetadata.Source, actual.ReceivePaymentsPubliclyMetadata.Source) @@ -223,7 +224,6 @@ 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.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) @@ -243,6 +243,7 @@ func testPublicDistributionRoundTrip(t *testing.T, s intent.Store) { expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.PublicDistribution, + MintAccount: "test_mint", InitiatorOwnerAccount: "test_owner", PublicDistributionMetadata: &intent.PublicDistributionMetadata{ Source: "test_source", @@ -261,9 +262,8 @@ func testPublicDistributionRoundTrip(t *testing.T, s intent.Store) { Quantity: 12345, UsdMarketValue: 999.99, }, - ExtendedMetadata: []byte("extended_metadata"), - State: intent.StateUnknown, - CreatedAt: time.Now(), + State: intent.StateUnknown, + CreatedAt: time.Now(), } cloned := expected.Clone() err = s.Save(ctx, &expected) @@ -281,6 +281,7 @@ func testPublicDistributionRoundTrip(t *testing.T, s intent.Store) { require.NoError(t, err) assert.Equal(t, cloned.IntentId, actual.IntentId) assert.Equal(t, cloned.IntentType, actual.IntentType) + assert.Equal(t, cloned.MintAccount, actual.MintAccount) assert.Equal(t, cloned.InitiatorOwnerAccount, actual.InitiatorOwnerAccount) require.NotNil(t, actual.PublicDistributionMetadata) assert.Equal(t, cloned.PublicDistributionMetadata.Source, actual.PublicDistributionMetadata.Source) @@ -292,7 +293,6 @@ func testPublicDistributionRoundTrip(t *testing.T, s intent.Store) { } assert.Equal(t, cloned.PublicDistributionMetadata.Quantity, actual.PublicDistributionMetadata.Quantity) assert.Equal(t, cloned.PublicDistributionMetadata.UsdMarketValue, actual.PublicDistributionMetadata.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) @@ -307,6 +307,7 @@ func testUpdateHappyPath(t *testing.T, s intent.Store) { expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.PublicDistribution, + MintAccount: "test_mint", InitiatorOwnerAccount: "test_owner", PublicDistributionMetadata: &intent.PublicDistributionMetadata{ Source: "test_source", @@ -357,6 +358,7 @@ func testUpdateStaleRecord(t *testing.T, s intent.Store) { expected := intent.Record{ IntentId: "test_intent_id", IntentType: intent.OpenAccounts, + MintAccount: "test_mint", InitiatorOwnerAccount: "test_owner", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, State: intent.StateUnknown, @@ -389,17 +391,17 @@ func testGetOriginalGiftCardIssuedIntent(t *testing.T, s intent.Store) { ctx := context.Background() records := []*intent.Record{ - {IntentId: "i1", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a1", DestinationOwnerAccount: "o1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i1", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a1", DestinationOwnerAccount: "o1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i2", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{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.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{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.ExternalDeposit, ExternalDepositMetadata: &intent.ExternalDepositMetadata{DestinationTokenAccount: "a2", Quantity: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "o2", State: intent.StateConfirmed}, + {IntentId: "i2", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i3", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: false, DestinationTokenAccount: "a2", DestinationOwnerAccount: "o2", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i4", IntentType: intent.ExternalDeposit, ExternalDepositMetadata: &intent.ExternalDepositMetadata{DestinationTokenAccount: "a2", Quantity: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i5", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i6", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i5", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i6", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a3", DestinationOwnerAccount: "o3", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i7", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StatePending}, - {IntentId: "i8", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, InitiatorOwnerAccount: "user", State: intent.StateRevoked}, + {IntentId: "i7", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StatePending}, + {IntentId: "i8", IntentType: intent.SendPublicPayment, SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{IsRemoteSend: true, DestinationTokenAccount: "a4", DestinationOwnerAccount: "o4", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateRevoked}, } for _, record := range records { @@ -430,16 +432,16 @@ func testGetGiftCardClaimedIntent(t *testing.T, s intent.Store) { 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: "i1", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a1", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", 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: "i2", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: false, Source: "a2", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", 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}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i4", 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: "i5", 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: "i4", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, + {IntentId: "i5", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a3", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateConfirmed}, - {IntentId: "i6", 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: "i7", 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}, + {IntentId: "i6", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StateRevoked}, + {IntentId: "i7", IntentType: intent.ReceivePaymentsPublicly, ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{IsRemoteSend: true, Source: "a4", Quantity: 1, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 1, OriginalNativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", InitiatorOwnerAccount: "user", State: intent.StatePending}, } for _, record := range records { @@ -476,14 +478,14 @@ func testGetTransactedAmountForAntiMoneyLaundering(t *testing.T, s intent.Store) assert.EqualValues(t, 0, usdMarketValue) records := []*intent.Record{ - {IntentId: "t1", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o1", DestinationTokenAccount: "a1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2, UsdMarketValue: 2}, State: intent.StateUnknown, CreatedAt: time.Now().Add(-1 * time.Minute)}, - {IntentId: "t2", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 10, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 20, UsdMarketValue: 20}, State: intent.StatePending, CreatedAt: time.Now().Add(-2 * time.Minute)}, - {IntentId: "t3", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o3", DestinationTokenAccount: "a3", Quantity: 100, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 200, UsdMarketValue: 200}, State: intent.StateConfirmed, CreatedAt: time.Now().Add(-3 * time.Minute)}, - {IntentId: "t4", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o4", DestinationTokenAccount: "a4", Quantity: 1000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2000, UsdMarketValue: 2000}, State: intent.StateFailed, CreatedAt: time.Now().Add(-4 * time.Minute)}, - {IntentId: "t5", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o5", DestinationTokenAccount: "a5", Quantity: 10000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 20000, UsdMarketValue: 20000}, State: intent.StateRevoked, CreatedAt: time.Now().Add(-5 * time.Minute)}, - {IntentId: "t6", IntentType: intent.ReceivePaymentsPublicly, InitiatorOwnerAccount: "o1", ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{Source: "a6", Quantity: 100000, UsdMarketValue: 200000, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 2, OriginalNativeAmount: 200000}, State: intent.StateConfirmed, CreatedAt: time.Now()}, - {IntentId: "t7", IntentType: intent.ExternalDeposit, InitiatorOwnerAccount: "o1", ExternalDepositMetadata: &intent.ExternalDepositMetadata{DestinationTokenAccount: "a7", Quantity: 1000000, UsdMarketValue: 20000}, State: intent.StateConfirmed, CreatedAt: time.Now()}, - {IntentId: "t8", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o8", DestinationTokenAccount: "a8", Quantity: 10000000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2000000, UsdMarketValue: 2000000, IsWithdrawal: true}, State: intent.StateConfirmed, CreatedAt: time.Now()}, + {IntentId: "t1", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o1", DestinationTokenAccount: "a1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2, UsdMarketValue: 2}, State: intent.StateUnknown, MintAccount: "mint", CreatedAt: time.Now().Add(-1 * time.Minute)}, + {IntentId: "t2", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 10, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 20, UsdMarketValue: 20}, State: intent.StatePending, MintAccount: "mint", CreatedAt: time.Now().Add(-2 * time.Minute)}, + {IntentId: "t3", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o3", DestinationTokenAccount: "a3", Quantity: 100, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 200, UsdMarketValue: 200}, State: intent.StateConfirmed, MintAccount: "mint", CreatedAt: time.Now().Add(-3 * time.Minute)}, + {IntentId: "t4", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o4", DestinationTokenAccount: "a4", Quantity: 1000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2000, UsdMarketValue: 2000}, State: intent.StateFailed, MintAccount: "mint", CreatedAt: time.Now().Add(-4 * time.Minute)}, + {IntentId: "t5", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o5", DestinationTokenAccount: "a5", Quantity: 10000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 20000, UsdMarketValue: 20000}, State: intent.StateRevoked, MintAccount: "mint", CreatedAt: time.Now().Add(-5 * time.Minute)}, + {IntentId: "t6", IntentType: intent.ReceivePaymentsPublicly, InitiatorOwnerAccount: "o1", ReceivePaymentsPubliclyMetadata: &intent.ReceivePaymentsPubliclyMetadata{Source: "a6", Quantity: 100000, UsdMarketValue: 200000, OriginalExchangeCurrency: currency.USD, OriginalExchangeRate: 2, OriginalNativeAmount: 200000}, State: intent.StateConfirmed, MintAccount: "mint", CreatedAt: time.Now()}, + {IntentId: "t7", IntentType: intent.ExternalDeposit, InitiatorOwnerAccount: "o1", ExternalDepositMetadata: &intent.ExternalDepositMetadata{DestinationTokenAccount: "a7", Quantity: 1000000, UsdMarketValue: 20000}, MintAccount: "mint", State: intent.StateConfirmed, CreatedAt: time.Now()}, + {IntentId: "t8", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o8", DestinationTokenAccount: "a8", Quantity: 10000000, ExchangeCurrency: currency.USD, ExchangeRate: 2, NativeAmount: 2000000, UsdMarketValue: 2000000, IsWithdrawal: true}, State: intent.StateConfirmed, MintAccount: "mint", CreatedAt: time.Now()}, } for _, record := range records { @@ -518,13 +520,13 @@ func testGetByOwner(t *testing.T, s intent.Store) { assert.Equal(t, intent.ErrIntentNotFound, err) records := []*intent.Record{ - {IntentId: "t1", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "o1", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, State: intent.StateConfirmed, CreatedAt: time.Now()}, - {IntentId: "t2", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o1", DestinationTokenAccount: "a1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, State: intent.StateConfirmed, CreatedAt: time.Now()}, - {IntentId: "t3", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 10, ExchangeCurrency: currency.USD, ExchangeRate: 10, NativeAmount: 10, UsdMarketValue: 10}, State: intent.StatePending, CreatedAt: time.Now()}, - {IntentId: "t4", IntentType: intent.PublicDistribution, InitiatorOwnerAccount: "o1", PublicDistributionMetadata: &intent.PublicDistributionMetadata{Source: "p1", Distributions: []*intent.Distribution{{DestinationOwnerAccount: "o1", DestinationTokenAccount: "a1", Quantity: 5}, {DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 5}}, UsdMarketValue: 10, Quantity: 10}, State: intent.StatePending, CreatedAt: time.Now()}, - {IntentId: "t5", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 100, ExchangeCurrency: currency.USD, ExchangeRate: 100, NativeAmount: 100, UsdMarketValue: 100}, State: intent.StateFailed, CreatedAt: time.Now()}, - {IntentId: "t6", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 1000, ExchangeCurrency: currency.USD, ExchangeRate: 1000, NativeAmount: 1000, UsdMarketValue: 1000}, State: intent.StateUnknown, CreatedAt: time.Now()}, - {IntentId: "t7", IntentType: intent.PublicDistribution, InitiatorOwnerAccount: "o1", PublicDistributionMetadata: &intent.PublicDistributionMetadata{Source: "p2", Distributions: []*intent.Distribution{{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 10}}, UsdMarketValue: 10, Quantity: 10}, State: intent.StatePending, CreatedAt: time.Now()}, + {IntentId: "t1", IntentType: intent.OpenAccounts, InitiatorOwnerAccount: "o1", OpenAccountsMetadata: &intent.OpenAccountsMetadata{}, MintAccount: "mint", State: intent.StateConfirmed, CreatedAt: time.Now()}, + {IntentId: "t2", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o1", DestinationTokenAccount: "a1", Quantity: 1, ExchangeCurrency: currency.USD, ExchangeRate: 1, NativeAmount: 1, UsdMarketValue: 1}, MintAccount: "mint", State: intent.StateConfirmed, CreatedAt: time.Now()}, + {IntentId: "t3", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 10, ExchangeCurrency: currency.USD, ExchangeRate: 10, NativeAmount: 10, UsdMarketValue: 10}, MintAccount: "mint", State: intent.StatePending, CreatedAt: time.Now()}, + {IntentId: "t4", IntentType: intent.PublicDistribution, InitiatorOwnerAccount: "o1", PublicDistributionMetadata: &intent.PublicDistributionMetadata{Source: "p1", Distributions: []*intent.Distribution{{DestinationOwnerAccount: "o1", DestinationTokenAccount: "a1", Quantity: 5}, {DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 5}}, UsdMarketValue: 10, Quantity: 10}, MintAccount: "mint", State: intent.StatePending, CreatedAt: time.Now()}, + {IntentId: "t5", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 100, ExchangeCurrency: currency.USD, ExchangeRate: 100, NativeAmount: 100, UsdMarketValue: 100}, MintAccount: "mint", State: intent.StateFailed, CreatedAt: time.Now()}, + {IntentId: "t6", IntentType: intent.SendPublicPayment, InitiatorOwnerAccount: "o1", SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 1000, ExchangeCurrency: currency.USD, ExchangeRate: 1000, NativeAmount: 1000, UsdMarketValue: 1000}, MintAccount: "mint", State: intent.StateUnknown, CreatedAt: time.Now()}, + {IntentId: "t7", IntentType: intent.PublicDistribution, InitiatorOwnerAccount: "o1", PublicDistributionMetadata: &intent.PublicDistributionMetadata{Source: "p2", Distributions: []*intent.Distribution{{DestinationOwnerAccount: "o2", DestinationTokenAccount: "a2", Quantity: 10}}, UsdMarketValue: 10, Quantity: 10}, MintAccount: "mint", State: intent.StatePending, CreatedAt: time.Now()}, } for _, record := range records { require.NoError(t, s.Save(ctx, record)) diff --git a/pkg/code/server/account/server.go b/pkg/code/server/account/server.go index ccf09a6e..842990ac 100644 --- a/pkg/code/server/account/server.go +++ b/pkg/code/server/account/server.go @@ -507,6 +507,7 @@ func (s *server) getOriginalGiftCardExchangeData(ctx context.Context, records *c ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate, NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount, Quarks: intentRecord.SendPublicPaymentMetadata.Quantity, + Mint: common.CoreMintAccount.ToProto(), }, nil } diff --git a/pkg/code/server/account/server_test.go b/pkg/code/server/account/server_test.go index 46d14f31..c144761f 100644 --- a/pkg/code/server/account/server_test.go +++ b/pkg/code/server/account/server_test.go @@ -64,6 +64,9 @@ func TestIsCodeAccount_HappyPath(t *testing.T) { env, cleanup := setup(t) defer cleanup() + coreVmConfig := testutil.NewRandomVmConfig(t, true) + swapVmConfig := testutil.NewRandomVmConfig(t, false) + ownerAccount := testutil.NewRandomAccount(t) swapAuthorityAccount := testutil.NewRandomAccount(t) @@ -82,8 +85,8 @@ func TestIsCodeAccount_HappyPath(t *testing.T) { // Technically an invalid reality, but SubmitIntent guarantees all or no accounts // are opened, which allows IsCodeAccount to do lazy checking. - setupAccountRecords(t, env, ownerAccount, ownerAccount, common.CoreMintAccount, 0, commonpb.AccountType_PRIMARY) - setupAccountRecords(t, env, ownerAccount, swapAuthorityAccount, testutil.NewRandomAccount(t), 0, commonpb.AccountType_SWAP) + setupAccountRecords(t, env, ownerAccount, ownerAccount, coreVmConfig, 0, commonpb.AccountType_PRIMARY) + setupAccountRecords(t, env, ownerAccount, swapAuthorityAccount, swapVmConfig, 0, commonpb.AccountType_SWAP) resp, err = env.client.IsCodeAccount(env.ctx, req) require.NoError(t, err) @@ -98,6 +101,8 @@ func TestIsCodeAccount_NotManagedByCode(t *testing.T) { env, cleanup := setup(t) defer cleanup() + coreVmConfig := testutil.NewRandomVmConfig(t, true) + ownerAccount := testutil.NewRandomAccount(t) req := &accountpb.IsCodeAccountRequest{ @@ -114,7 +119,7 @@ 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, common.CoreMintAccount, 0, commonpb.AccountType_PRIMARY)) + allAccountRecords = append(allAccountRecords, setupAccountRecords(t, env, ownerAccount, ownerAccount, coreVmConfig, 0, commonpb.AccountType_PRIMARY)) resp, err = env.client.IsCodeAccount(env.ctx, req) require.NoError(t, err) @@ -134,6 +139,10 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { env, cleanup := setup(t) defer cleanup() + coreVmConfig := testutil.NewRandomVmConfig(t, true) + jeffyVmConfig := testutil.NewRandomVmConfig(t, false) + swapVmConfig := testutil.NewRandomVmConfig(t, false) + ownerAccount := testutil.NewRandomAccount(t) req := &accountpb.GetTokenAccountInfosRequest{ @@ -149,14 +158,11 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { poolAuthority2 := testutil.NewRandomAccount(t) swapAuthority := testutil.NewRandomAccount(t) - jeffyMint := testutil.NewRandomAccount(t) - swapMint := testutil.NewRandomAccount(t) - - primaryCoreMintAccountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, common.CoreMintAccount, 0, commonpb.AccountType_PRIMARY) - pool1CoreMintAccountRecords := setupAccountRecords(t, env, ownerAccount, poolAuthority1, common.CoreMintAccount, 0, commonpb.AccountType_POOL) - pool2CoreMintAccountRecords := setupAccountRecords(t, env, ownerAccount, poolAuthority2, common.CoreMintAccount, 1, commonpb.AccountType_POOL) - primaryJeffyMintAccountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, jeffyMint, 0, commonpb.AccountType_PRIMARY) - setupAccountRecords(t, env, ownerAccount, swapAuthority, swapMint, 0, commonpb.AccountType_SWAP) + primaryCoreMintAccountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, coreVmConfig, 0, commonpb.AccountType_PRIMARY) + pool1CoreMintAccountRecords := setupAccountRecords(t, env, ownerAccount, poolAuthority1, coreVmConfig, 0, commonpb.AccountType_POOL) + pool2CoreMintAccountRecords := setupAccountRecords(t, env, ownerAccount, poolAuthority2, coreVmConfig, 1, commonpb.AccountType_POOL) + primaryJeffyMintAccountRecords := setupAccountRecords(t, env, ownerAccount, ownerAccount, jeffyVmConfig, 0, commonpb.AccountType_PRIMARY) + setupAccountRecords(t, env, ownerAccount, swapAuthority, swapVmConfig, 0, commonpb.AccountType_SWAP) setupCachedBalance(t, env, primaryCoreMintAccountRecords, common.ToCoreMintQuarks(42)) setupCachedBalance(t, env, pool1CoreMintAccountRecords, common.ToCoreMintQuarks(88)) @@ -171,20 +177,20 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { for _, tc := range []struct { authority *common.Account - mints []*common.Account + vmConfigs []*common.VmConfig }{ - {ownerAccount, []*common.Account{common.CoreMintAccount, jeffyMint}}, - {poolAuthority1, []*common.Account{common.CoreMintAccount}}, - {poolAuthority2, []*common.Account{common.CoreMintAccount}}, - {swapAuthority, []*common.Account{swapMint}}, + {ownerAccount, []*common.VmConfig{coreVmConfig, jeffyVmConfig}}, + {poolAuthority1, []*common.VmConfig{coreVmConfig}}, + {poolAuthority2, []*common.VmConfig{coreVmConfig}}, + {swapAuthority, []*common.VmConfig{swapVmConfig}}, } { - for _, mint := range tc.mints { + for _, vmConfig := range tc.vmConfigs { var tokenAccount *common.Account if tc.authority.PublicKey().ToBase58() == swapAuthority.PublicKey().ToBase58() { - tokenAccount, err = tc.authority.ToAssociatedTokenAccount(mint) + tokenAccount, err = tc.authority.ToAssociatedTokenAccount(vmConfig.Mint) require.NoError(t, err) } else { - timelockAccounts, err := tc.authority.GetTimelockAccounts(testutil.NewRandomAccount(t), mint) + timelockAccounts, err := tc.authority.GetTimelockAccounts(vmConfig) require.NoError(t, err) tokenAccount = timelockAccounts.Vault } @@ -195,16 +201,16 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { assert.Equal(t, tokenAccount.PublicKey().ToBytes(), accountInfo.Address.Value) assert.Equal(t, ownerAccount.PublicKey().ToBytes(), accountInfo.Owner.Value) assert.Equal(t, tc.authority.PublicKey().ToBytes(), accountInfo.Authority.Value) - assert.Equal(t, mint.PublicKey().ToBytes(), accountInfo.Mint.Value) + assert.Equal(t, vmConfig.Mint.PublicKey().ToBytes(), accountInfo.Mint.Value) switch tc.authority.PublicKey().ToBase58() { case ownerAccount.PublicKey().ToBase58(): assert.Equal(t, commonpb.AccountType_PRIMARY, accountInfo.AccountType) assert.EqualValues(t, 0, accountInfo.Index) - switch mint.PublicKey().ToBase58() { + switch vmConfig.Mint.PublicKey().ToBase58() { case common.CoreMintAccount.PublicKey().ToBase58(): assert.EqualValues(t, common.ToCoreMintQuarks(42), accountInfo.Balance) - case jeffyMint.PublicKey().ToBase58(): + case jeffyVmConfig.Mint.PublicKey().ToBase58(): assert.EqualValues(t, currencycreator.ToQuarks(98765), accountInfo.Balance) default: require.Fail(t, "unexpected mint") @@ -243,13 +249,15 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { primaryAccountInfoRecordsByMint, err := env.data.GetLatestAccountInfoByOwnerAddressAndType(env.ctx, ownerAccount.PublicKey().ToBase58(), commonpb.AccountType_PRIMARY) require.NoError(t, err) assert.True(t, primaryAccountInfoRecordsByMint[common.CoreMintAccount.PublicKey().ToBase58()].RequiresDepositSync) - assert.True(t, primaryAccountInfoRecordsByMint[jeffyMint.PublicKey().ToBase58()].RequiresDepositSync) + assert.True(t, primaryAccountInfoRecordsByMint[jeffyVmConfig.Mint.PublicKey().ToBase58()].RequiresDepositSync) } func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { env, cleanup := setup(t) defer cleanup() + coreVmConfig := testutil.NewRandomVmConfig(t, true) + // 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. @@ -377,12 +385,14 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { giftCardIssuerOwnerAccount := testutil.NewRandomAccount(t) giftCardOwnerAccount := testutil.NewRandomAccount(t) - accountRecords := setupAccountRecords(t, env, giftCardOwnerAccount, giftCardOwnerAccount, common.CoreMintAccount, 0, commonpb.AccountType_REMOTE_SEND_GIFT_CARD) + accountRecords := setupAccountRecords(t, env, giftCardOwnerAccount, giftCardOwnerAccount, coreVmConfig, 0, commonpb.AccountType_REMOTE_SEND_GIFT_CARD) giftCardIssuedIntentRecord := &intent.Record{ IntentId: testutil.NewRandomAccount(t).PublicKey().ToBase58(), IntentType: intent.SendPublicPayment, + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), + InitiatorOwnerAccount: giftCardIssuerOwnerAccount.PublicKey().ToBase58(), SendPublicPaymentMetadata: &intent.SendPublicPaymentMetadata{ @@ -450,7 +460,7 @@ func TestGetTokenAccountInfos_RemoteSendGiftCard_HappyPath(t *testing.T) { testutil.NewRandomAccount(t), giftCardIssuerOwnerAccount, } { - timelockAccounts, err := giftCardOwnerAccount.GetTimelockAccounts(testutil.NewRandomAccount(t), common.CoreMintAccount) + timelockAccounts, err := giftCardOwnerAccount.GetTimelockAccounts(coreVmConfig) require.NoError(t, err) req := &accountpb.GetTokenAccountInfosRequest{ @@ -520,6 +530,8 @@ func TestGetTokenAccountInfos_BlockchainState(t *testing.T) { env, cleanup := setup(t) defer cleanup() + coreVmConfig := testutil.NewRandomVmConfig(t, true) + for _, tc := range []struct { timelockState timelock_token_v1.TimelockState expected accountpb.TokenAccountInfo_BlockchainState @@ -541,7 +553,7 @@ func TestGetTokenAccountInfos_BlockchainState(t *testing.T) { Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), } - accountRecords := getDefaultTestAccountRecords(t, ownerAccount, ownerAccount, common.CoreMintAccount, 0, commonpb.AccountType_PRIMARY) + accountRecords := getDefaultTestAccountRecords(t, ownerAccount, ownerAccount, coreVmConfig, 0, commonpb.AccountType_PRIMARY) accountRecords.Timelock.VaultState = tc.timelockState accountRecords.Timelock.Block += 1 require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountRecords.General)) @@ -562,6 +574,8 @@ func TestGetTokenAccountInfos_ManagementState(t *testing.T) { env, cleanup := setup(t) defer cleanup() + coreVmConfig := testutil.NewRandomVmConfig(t, true) + for _, tc := range []struct { timelockState timelock_token_v1.TimelockState block uint64 @@ -607,7 +621,7 @@ func TestGetTokenAccountInfos_ManagementState(t *testing.T) { Value: ed25519.Sign(ownerAccount.PrivateKey().ToBytes(), reqBytes), } - accountRecords := getDefaultTestAccountRecords(t, ownerAccount, ownerAccount, common.CoreMintAccount, 0, commonpb.AccountType_PRIMARY) + accountRecords := getDefaultTestAccountRecords(t, ownerAccount, ownerAccount, coreVmConfig, 0, commonpb.AccountType_PRIMARY) accountRecords.Timelock.VaultState = tc.timelockState accountRecords.Timelock.Block = tc.block require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountRecords.General)) @@ -709,8 +723,8 @@ func TestUnauthenticatedRPC(t *testing.T) { testutil.AssertStatusErrorWithCode(t, err, codes.Unauthenticated) } -func setupAccountRecords(t *testing.T, env testEnv, ownerAccount, authorityAccount, mintAccount *common.Account, index uint64, accountType commonpb.AccountType) *common.AccountRecords { - accountRecords := getDefaultTestAccountRecords(t, ownerAccount, authorityAccount, mintAccount, index, accountType) +func setupAccountRecords(t *testing.T, env testEnv, ownerAccount, authorityAccount *common.Account, vmConfig *common.VmConfig, index uint64, accountType commonpb.AccountType) *common.AccountRecords { + accountRecords := getDefaultTestAccountRecords(t, ownerAccount, authorityAccount, vmConfig, index, accountType) require.NoError(t, env.data.CreateAccountInfo(env.ctx, accountRecords.General)) @@ -723,16 +737,16 @@ func setupAccountRecords(t *testing.T, env testEnv, ownerAccount, authorityAccou return accountRecords } -func getDefaultTestAccountRecords(t *testing.T, ownerAccount, authorityAccount, mintAccount *common.Account, index uint64, accountType commonpb.AccountType) *common.AccountRecords { +func getDefaultTestAccountRecords(t *testing.T, ownerAccount, authorityAccount *common.Account, vmConfig *common.VmConfig, index uint64, accountType commonpb.AccountType) *common.AccountRecords { var tokenAccount *common.Account var timelockRecord *timelock.Record var err error if accountType == commonpb.AccountType_SWAP { - tokenAccount, err = authorityAccount.ToAssociatedTokenAccount(mintAccount) + tokenAccount, err = authorityAccount.ToAssociatedTokenAccount(vmConfig.Mint) require.NoError(t, err) } else { - timelockAccounts, err := authorityAccount.GetTimelockAccounts(testutil.NewRandomAccount(t), mintAccount) + timelockAccounts, err := authorityAccount.GetTimelockAccounts(vmConfig) require.NoError(t, err) timelockRecord = timelockAccounts.ToDBRecord() @@ -743,7 +757,7 @@ func getDefaultTestAccountRecords(t *testing.T, ownerAccount, authorityAccount, OwnerAccount: ownerAccount.PublicKey().ToBase58(), AuthorityAccount: authorityAccount.PublicKey().ToBase58(), TokenAccount: tokenAccount.PublicKey().ToBase58(), - MintAccount: mintAccount.PublicKey().ToBase58(), + MintAccount: vmConfig.Mint.PublicKey().ToBase58(), AccountType: accountType, diff --git a/pkg/code/server/transaction/action_handler.go b/pkg/code/server/transaction/action_handler.go index 920a54db..94da7453 100644 --- a/pkg/code/server/transaction/action_handler.go +++ b/pkg/code/server/transaction/action_handler.go @@ -58,8 +58,9 @@ type CreateActionHandler interface { // 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 + // instruction needs to be signed by the client. The VM where the action + // will take place is provided when the result is true. + RequiresNonce(fulfillmentIndex int) (bool, *common.Account) // GetFulfillmentMetadata gets metadata for the fulfillment being created GetFulfillmentMetadata( @@ -85,7 +86,17 @@ type OpenAccountActionHandler struct { unsavedTimelockRecord *timelock.Record } -func NewOpenAccountActionHandler(data code_data.Provider, protoAction *transactionpb.OpenAccountAction, protoMetadata *transactionpb.Metadata) (CreateActionHandler, error) { +func NewOpenAccountActionHandler(ctx context.Context, data code_data.Provider, protoAction *transactionpb.OpenAccountAction, protoMetadata *transactionpb.Metadata) (CreateActionHandler, error) { + mint, err := common.GetBackwardsCompatMint(protoAction.Mint) + if err != nil { + return nil, err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, data, mint) + if err != nil { + return nil, err + } + owner, err := common.NewAccountFromProto(protoAction.Owner) if err != nil { return nil, err @@ -96,7 +107,7 @@ func NewOpenAccountActionHandler(data code_data.Provider, protoAction *transacti return nil, err } - timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + timelockAccounts, err := authority.GetTimelockAccounts(vmConfig) if err != nil { return nil, err } @@ -146,8 +157,8 @@ func (h *OpenAccountActionHandler) GetServerParameter() *transactionpb.ServerPar } } -func (h *OpenAccountActionHandler) RequiresNonce(index int) bool { - return false +func (h *OpenAccountActionHandler) RequiresNonce(index int) (bool, *common.Account) { + return false, nil } func (h *OpenAccountActionHandler) GetFulfillmentMetadata( @@ -189,13 +200,23 @@ type NoPrivacyTransferActionHandler struct { feeType transactionpb.FeePaymentAction_FeeType // Internally, the mechanics of a fee payment are exactly the same } -func NewNoPrivacyTransferActionHandler(protoAction *transactionpb.NoPrivacyTransferAction) (CreateActionHandler, error) { +func NewNoPrivacyTransferActionHandler(ctx context.Context, data code_data.Provider, protoAction *transactionpb.NoPrivacyTransferAction) (CreateActionHandler, error) { + mint, err := common.GetBackwardsCompatMint(protoAction.Mint) + if err != nil { + return nil, err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, data, mint) + if err != nil { + return nil, err + } + sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) if err != nil { return nil, err } - source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + source, err := sourceAuthority.GetTimelockAccounts(vmConfig) if err != nil { return nil, err } @@ -212,13 +233,23 @@ func NewNoPrivacyTransferActionHandler(protoAction *transactionpb.NoPrivacyTrans }, nil } -func NewFeePaymentActionHandler(protoAction *transactionpb.FeePaymentAction, feeCollector *common.Account) (CreateActionHandler, error) { +func NewFeePaymentActionHandler(ctx context.Context, data code_data.Provider, protoAction *transactionpb.FeePaymentAction, feeCollector *common.Account) (CreateActionHandler, error) { + mint, err := common.GetBackwardsCompatMint(protoAction.Mint) + if err != nil { + return nil, err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, data, mint) + if err != nil { + return nil, err + } + sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) if err != nil { return nil, err } - source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + source, err := sourceAuthority.GetTimelockAccounts(vmConfig) if err != nil { return nil, err } @@ -268,8 +299,8 @@ func (h *NoPrivacyTransferActionHandler) GetServerParameter() *transactionpb.Ser } } -func (h *NoPrivacyTransferActionHandler) RequiresNonce(index int) bool { - return true +func (h *NoPrivacyTransferActionHandler) RequiresNonce(index int) (bool, *common.Account) { + return true, h.source.Vm } func (h *NoPrivacyTransferActionHandler) GetFulfillmentMetadata( @@ -317,13 +348,23 @@ type NoPrivacyWithdrawActionHandler struct { isAutoReturn bool } -func NewNoPrivacyWithdrawActionHandler(intentRecord *intent.Record, protoAction *transactionpb.NoPrivacyWithdrawAction) (CreateActionHandler, error) { +func NewNoPrivacyWithdrawActionHandler(ctx context.Context, data code_data.Provider, intentRecord *intent.Record, protoAction *transactionpb.NoPrivacyWithdrawAction) (CreateActionHandler, error) { + mint, err := common.GetBackwardsCompatMint(protoAction.Mint) + if err != nil { + return nil, err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, data, mint) + if err != nil { + return nil, err + } + sourceAuthority, err := common.NewAccountFromProto(protoAction.Authority) if err != nil { return nil, err } - source, err := sourceAuthority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + source, err := sourceAuthority.GetTimelockAccounts(vmConfig) if err != nil { return nil, err } @@ -373,8 +414,8 @@ func (h *NoPrivacyWithdrawActionHandler) GetServerParameter() *transactionpb.Ser } } -func (h *NoPrivacyWithdrawActionHandler) RequiresNonce(index int) bool { - return true +func (h *NoPrivacyWithdrawActionHandler) RequiresNonce(index int) (bool, *common.Account) { + return true, h.source.Vm } func (h *NoPrivacyWithdrawActionHandler) GetFulfillmentMetadata( diff --git a/pkg/code/server/transaction/airdrop.go b/pkg/code/server/transaction/airdrop.go index b390679c..dbffad9c 100644 --- a/pkg/code/server/transaction/airdrop.go +++ b/pkg/code/server/transaction/airdrop.go @@ -23,9 +23,10 @@ import ( currency_util "github.com/code-payments/code-server/pkg/code/currency" "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/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/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/pointer" @@ -137,6 +138,7 @@ func (s *transactionServer) Airdrop(ctx context.Context, req *transactionpb.Aird ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate, NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount, Quarks: intentRecord.SendPublicPaymentMetadata.Quantity, + Mint: common.CoreMintAccount.ToProto(), }, }, nil } @@ -193,42 +195,32 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner "currency": currencyCode, }) - exchangeRateTime := currency_util.GetLatestExchangeRateTime() - - usdRateRecord, err := s.data.GetExchangeRate(ctx, currency_lib.USD, exchangeRateTime) - if err != nil { - log.WithError(err).Warn("failure getting usd rate") - return nil, err - } - var additionalQuarks uint64 - var otherRateRecord *currency.ExchangeRateRecord switch currencyCode { case currency_lib.USD: if !common.IsCoreMintUsdStableCoin() { additionalQuarks = 1 } - otherRateRecord = usdRateRecord - case common.CoreMintSymbol: - otherRateRecord = ¤cy.ExchangeRateRecord{ - Time: exchangeRateTime, - Rate: 1.0, - Symbol: string(common.CoreMintSymbol), - } default: additionalQuarks = 1 - otherRateRecord, err = s.data.GetExchangeRate(ctx, currencyCode, exchangeRateTime) - if err != nil { - log.WithError(err).Warn("failure getting other rate") - return nil, err - } } - coreMintAmount := nativeAmount / otherRateRecord.Rate + exchangeRateRecord, err := s.data.GetExchangeRate(ctx, currencyCode, currency_util.GetLatestExchangeRateTime()) + if err != nil { + log.WithError(err).Warn("failure getting other rate") + return nil, err + } + + coreMintAmount := nativeAmount / exchangeRateRecord.Rate quarkAmount := uint64(coreMintAmount*float64(common.CoreMintQuarksPerUnit)) + additionalQuarks - usdValue := usdRateRecord.Rate * coreMintAmount - if usdValue > s.conf.maxAirdropUsdValue.Get(ctx) { + usdMarketValue, err := currency_util.CalculateUsdMarketValue(ctx, s.data, common.CoreMintAccount, quarkAmount, currency_util.GetLatestExchangeRateTime()) + if err != nil { + log.WithError(err).Warn("failure calculating usd market value") + return nil, err + } + + if usdMarketValue > s.conf.maxAirdropUsdValue.Get(ctx) { log.Warn("airdrop exceeds max usd value") return nil, ErrIneligibleForAirdrop } @@ -278,7 +270,17 @@ 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 := s.noncePool.GetNonce(ctx) + noncePool, err := transaction.SelectNoncePool( + nonce.EnvironmentCvm, + common.CodeVmAccount.PublicKey().ToBase58(), + nonce.PurposeClientTransaction, + s.noncePools..., + ) + if err != nil { + log.WithError(err).Warn("failure selecting nonce pool") + return nil, err + } + selectedNonce, err := noncePool.GetNonce(ctx) if err != nil { log.WithError(err).Warn("failure selecting available nonce") return nil, err @@ -306,13 +308,15 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner Quantity: quarkAmount, ExchangeCurrency: currencyCode, - ExchangeRate: otherRateRecord.Rate, + ExchangeRate: exchangeRateRecord.Rate, NativeAmount: nativeAmount, - UsdMarketValue: usdValue, + UsdMarketValue: usdMarketValue, IsWithdrawal: false, }, + MintAccount: common.CoreMintAccount.PublicKey().ToBase58(), + InitiatorOwnerAccount: s.airdropper.VaultOwner.PublicKey().ToBase58(), State: intent.StatePending, @@ -408,6 +412,11 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner } func (s *transactionServer) loadAirdropper(ctx context.Context) error { + vmConfig, err := common.GetVmConfigForMint(ctx, s.data, common.CoreMintAccount) + if err != nil { + return err + } + vaultRecord, err := s.data.GetKey(ctx, s.conf.airdropperOwnerPublicKey.Get(ctx)) if err != nil { return err @@ -418,7 +427,7 @@ func (s *transactionServer) loadAirdropper(ctx context.Context) error { return err } - timelockAccounts, err := ownerAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + timelockAccounts, err := ownerAccount.GetTimelockAccounts(vmConfig) if err != nil { return err } diff --git a/pkg/code/server/transaction/errors.go b/pkg/code/server/transaction/errors.go index c5ed22d4..52144397 100644 --- a/pkg/code/server/transaction/errors.go +++ b/pkg/code/server/transaction/errors.go @@ -227,7 +227,7 @@ func handleSubmitIntentError(streamer transactionpb.Transaction_SubmitIntentServ return status.Error(codes.DeadlineExceeded, err.Error()) case context.Canceled: return status.Error(codes.Canceled, err.Error()) - case transaction.ErrNoAvailableNonces: + case transaction.ErrNoAvailableNonces, transaction.ErrNoncePoolNotFound: return status.Error(codes.Unavailable, "") } return status.Error(codes.Internal, "rpc server failure") diff --git a/pkg/code/server/transaction/intent.go b/pkg/code/server/transaction/intent.go index 87bf0906..6c082e8a 100644 --- a/pkg/code/server/transaction/intent.go +++ b/pkg/code/server/transaction/intent.go @@ -27,6 +27,7 @@ import ( "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/code/data/timelock" "github.com/code-payments/code-server/pkg/code/transaction" currency_lib "github.com/code-payments/code-server/pkg/currency" @@ -343,19 +344,19 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm case *transactionpb.Action_OpenAccount: log = log.WithField("action_type", "open_account") actionType = action.OpenAccount - actionHandler, err = NewOpenAccountActionHandler(s.data, typed.OpenAccount, submitActionsReq.Metadata) + actionHandler, err = NewOpenAccountActionHandler(ctx, s.data, typed.OpenAccount, submitActionsReq.Metadata) case *transactionpb.Action_NoPrivacyTransfer: log = log.WithField("action_type", "no_privacy_transfer") actionType = action.NoPrivacyTransfer - actionHandler, err = NewNoPrivacyTransferActionHandler(typed.NoPrivacyTransfer) + actionHandler, err = NewNoPrivacyTransferActionHandler(ctx, s.data, typed.NoPrivacyTransfer) case *transactionpb.Action_FeePayment: log = log.WithField("action_type", "fee_payment") actionType = action.NoPrivacyTransfer - actionHandler, err = NewFeePaymentActionHandler(typed.FeePayment, s.feeCollector) + actionHandler, err = NewFeePaymentActionHandler(ctx, s.data, typed.FeePayment, s.feeCollector) case *transactionpb.Action_NoPrivacyWithdraw: log = log.WithField("action_type", "no_privacy_withdraw") actionType = action.NoPrivacyWithdraw - actionHandler, err = NewNoPrivacyWithdrawActionHandler(intentRecord, typed.NoPrivacyWithdraw) + actionHandler, err = NewNoPrivacyWithdrawActionHandler(ctx, s.data, intentRecord, typed.NoPrivacyWithdraw) default: return handleSubmitIntentError(streamer, status.Errorf(codes.InvalidArgument, "SubmitIntentRequest.SubmitActions.Actions[%d].Type is nil", i)) } @@ -392,7 +393,7 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm fulfillmentCount := actionHandler.FulfillmentCount() - for j := 0; j < fulfillmentCount; j++ { + for j := range fulfillmentCount { var newFulfillmentMetadata *newFulfillmentMetadata var actionId uint32 @@ -401,8 +402,20 @@ func (s *transactionServer) SubmitIntent(streamer transactionpb.Transaction_Subm var selectedNonce *transaction.Nonce var nonceAccount *common.Account var nonceBlockchash solana.Blockhash - if actionHandler.RequiresNonce(j) { - selectedNonce, err = s.noncePool.GetNonce(ctx) + requiresNonce, vmAccount := actionHandler.RequiresNonce(j) + if requiresNonce { + noncePool, err := transaction.SelectNoncePool( + nonce.EnvironmentCvm, + vmAccount.PublicKey().ToBase58(), + nonce.PurposeClientTransaction, + s.noncePools..., + ) + if err != nil { + log.WithError(err).Warn("failure selecting nonce pool") + return handleSubmitIntentError(streamer, err) + } + + selectedNonce, err = noncePool.GetNonce(ctx) if err != nil { log.WithError(err).Warn("failure selecting available nonce") return handleSubmitIntentError(streamer, err) @@ -762,20 +775,19 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact var metadata *transactionpb.Metadata switch intentRecord.IntentType { - case intent.OpenAccounts: - metadata = &transactionpb.Metadata{ - Type: &transactionpb.Metadata_OpenAccounts{ - OpenAccounts: &transactionpb.OpenAccountsMetadata{}, - }, + case intent.SendPublicPayment: + mintAccount, err := common.NewAccountFromPublicKeyString(intentRecord.MintAccount) + if err != nil { + log.WithError(err).Warn("invalid mint account") + return nil, status.Error(codes.Internal, "") } - case intent.SendPublicPayment: sourceAccountInfoRecordsByMint, err := s.data.GetAccountInfoByAuthorityAddress(ctx, intentRecord.InitiatorOwnerAccount) if err != nil { log.WithError(err).Warn("failure getting source account info record") return nil, status.Error(codes.Internal, "") } - coreMintSourceAccountInfoRecord, ok := sourceAccountInfoRecordsByMint[common.CoreMintAccount.PublicKey().ToBase58()] + coreMintSourceAccountInfoRecord, ok := sourceAccountInfoRecordsByMint[mintAccount.PublicKey().ToBase58()] if !ok { log.WithError(err).Warn("core mint source account info record doesn't exist") return nil, status.Error(codes.Internal, "") @@ -803,15 +815,24 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact ExchangeRate: intentRecord.SendPublicPaymentMetadata.ExchangeRate, NativeAmount: intentRecord.SendPublicPaymentMetadata.NativeAmount, Quarks: intentRecord.SendPublicPaymentMetadata.Quantity, + Mint: mintAccount.ToProto(), }, + IsRemoteSend: intentRecord.SendPublicPaymentMetadata.IsRemoteSend, IsWithdrawal: intentRecord.SendPublicPaymentMetadata.IsWithdrawal, + Mint: mintAccount.ToProto(), }, }, } case intent.ReceivePaymentsPublicly: + mintAccount, err := common.NewAccountFromPublicKeyString(intentRecord.MintAccount) + if err != nil { + log.WithError(err).Warn("invalid mint account") + return nil, status.Error(codes.Internal, "") + } + sourceAccount, err := common.NewAccountFromPublicKeyString(intentRecord.ReceivePaymentsPubliclyMetadata.Source) if err != nil { - log.WithError(err).Warn("invalid destination account") + log.WithError(err).Warn("invalid source account") return nil, status.Error(codes.Internal, "") } @@ -826,12 +847,14 @@ func (s *transactionServer) GetIntentMetadata(ctx context.Context, req *transact ExchangeRate: intentRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate, NativeAmount: intentRecord.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount, Quarks: intentRecord.ReceivePaymentsPubliclyMetadata.Quantity, + Mint: mintAccount.ToProto(), }, + Mint: mintAccount.ToProto(), }, }, } default: - // This is not a client-initiated intent type. Don't reveal anything. + // Don't reveal anything for these intent types return &transactionpb.GetIntentMetadataResponse{ Result: transactionpb.GetIntentMetadataResponse_NOT_FOUND, }, nil @@ -1027,7 +1050,13 @@ func (s *transactionServer) VoidGiftCard(ctx context.Context, req *transactionpb claimedActionRecord, err := s.data.GetGiftCardClaimedAction(ctx, giftCardVault.PublicKey().ToBase58()) if err == nil { - ownerTimelockAccounts, err := owner.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + vmConfig, err := common.GetVmConfigForMint(ctx, s.data, common.CoreMintAccount) + if err != nil { + log.WithError(err).Warn("failure getting vm config") + return nil, status.Error(codes.Internal, "") + } + + ownerTimelockAccounts, err := owner.GetTimelockAccounts(vmConfig) if err != nil { log.WithError(err).Warn("failure getting owner timelock accounts") return nil, status.Error(codes.Internal, "") diff --git a/pkg/code/server/transaction/intent_handler.go b/pkg/code/server/transaction/intent_handler.go index 945602a6..e6dfe020 100644 --- a/pkg/code/server/transaction/intent_handler.go +++ b/pkg/code/server/transaction/intent_handler.go @@ -39,6 +39,8 @@ 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. + // + // Intent-level validation errors may be returned here if caught early. PopulateMetadata(ctx context.Context, intentRecord *intent.Record, protoMetadata *transactionpb.Metadata) error // CreatesNewUser returns whether the intent creates a new Code user identified @@ -81,7 +83,16 @@ func (h *OpenAccountsIntentHandler) PopulateMetadata(ctx context.Context, intent return errors.New("unexpected metadata proto message") } + mint, err := common.GetBackwardsCompatMint(typedProtoMetadata.Mint) + if err != nil { + return err + } + if !common.IsCoreMint(mint) { + return NewIntentValidationError("only the core mint is supported") + } + intentRecord.IntentType = intent.OpenAccounts + intentRecord.MintAccount = mint.PublicKey().ToBase58() intentRecord.OpenAccountsMetadata = &intent.OpenAccountsMetadata{} return nil @@ -96,6 +107,7 @@ func (h *OpenAccountsIntentHandler) CreatesNewUser(ctx context.Context, metadata return typedMetadata.AccountSet == transactionpb.OpenAccountsMetadata_USER, nil } +// todo: Not all multi-mint validation checks are implemented func (h *OpenAccountsIntentHandler) IsNoop(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) (bool, error) { typedMetadata := metadata.GetOpenAccounts() if typedMetadata == nil { @@ -144,6 +156,7 @@ func (h *OpenAccountsIntentHandler) GetBalanceLocks(ctx context.Context, intentR return nil, nil } +// todo: Not all multi-mint validation checks are implemented func (h *OpenAccountsIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, metadata *transactionpb.Metadata, actions []*transactionpb.Action) error { typedMetadata := metadata.GetOpenAccounts() if typedMetadata == nil { @@ -199,6 +212,15 @@ func (h *OpenAccountsIntentHandler) validateActions( typedMetadata *transactionpb.OpenAccountsMetadata, actions []*transactionpb.Action, ) error { + intentMint, err := common.GetBackwardsCompatMint(typedMetadata.Mint) + if err != nil { + return err + } + err = validateIntentAndActionMintsMatch(intentMint, actions) + if err != nil { + return err + } + type expectedAccountToOpen struct { Type commonpb.AccountType Index uint64 @@ -274,7 +296,7 @@ func (h *OpenAccountsIntentHandler) validateActions( } } - expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) + expectedVaultAccount, err := getExpectedTimelockVaultFromProtoAccounts(ctx, h.data, openAction.GetOpenAccount().Authority, openAction.GetFeePayment().Mint) if err != nil { return err } @@ -320,11 +342,22 @@ func (h *SendPublicPaymentIntentHandler) PopulateMetadata(ctx context.Context, i return errors.New("unexpected metadata proto message") } + mint, err := common.GetBackwardsCompatMint(typedProtoMetadata.Mint) + if err != nil { + return err + } + if !common.IsCoreMint(mint) && typedProtoMetadata.IsRemoteSend { + return NewIntentValidationError("only the core mint is supported for remote send") + } + if !common.IsCoreMint(mint) && typedProtoMetadata.IsWithdrawal { + return NewIntentValidationError("only the core mint is supported for withdrawals") + } + exchangeData := typedProtoMetadata.ExchangeData - usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, currency_util.GetLatestExchangeRateTime()) + usdMarketValue, err := currency_util.CalculateUsdMarketValue(ctx, h.data, mint, exchangeData.Quarks, currency_util.GetLatestExchangeRateTime()) if err != nil { - return errors.Wrap(err, "error getting current usd exchange rate") + return err } destination, err := common.NewAccountFromProto(typedProtoMetadata.Destination) @@ -339,6 +372,7 @@ func (h *SendPublicPaymentIntentHandler) PopulateMetadata(ctx context.Context, i h.cachedDestinationAccountInfoRecord = destinationAccountInfo intentRecord.IntentType = intent.SendPublicPayment + intentRecord.MintAccount = mint.PublicKey().ToBase58() intentRecord.SendPublicPaymentMetadata = &intent.SendPublicPaymentMetadata{ DestinationTokenAccount: destination.PublicKey().ToBase58(), Quantity: exchangeData.Quarks, @@ -346,7 +380,7 @@ func (h *SendPublicPaymentIntentHandler) PopulateMetadata(ctx context.Context, i ExchangeCurrency: currency_lib.Code(exchangeData.Currency), ExchangeRate: exchangeData.ExchangeRate, NativeAmount: typedProtoMetadata.ExchangeData.NativeAmount, - UsdMarketValue: usdExchangeRecord.Rate * float64(exchangeData.Quarks) / float64(common.CoreMintQuarksPerUnit), + UsdMarketValue: usdMarketValue, IsWithdrawal: typedProtoMetadata.IsWithdrawal, IsRemoteSend: typedProtoMetadata.IsRemoteSend, @@ -427,7 +461,7 @@ func (h *SendPublicPaymentIntentHandler) GetBalanceLocks(ctx context.Context, in return intentBalanceLocks, nil } -// todo: validation against Flipcash through generic interface for bet creation +// todo: Not all multi-mint validation checks are implemented func (h *SendPublicPaymentIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action) error { typedMetadata := untypedMetadata.GetSendPublicPayment() if typedMetadata == nil { @@ -438,14 +472,18 @@ func (h *SendPublicPaymentIntentHandler) AllowCreation(ctx context.Context, inte if err != nil { return err } + intentMintAccount, err := common.GetBackwardsCompatMint(typedMetadata.Mint) + if err != nil { + return err + } initiatorAccountsByMintAndType, err := common.GetLatestCodeTimelockAccountRecordsForOwner(ctx, h.data, initiatiorOwnerAccount) if err != nil { return err } - initiatorAccountsByType, ok := initiatorAccountsByMintAndType[common.CoreMintAccount.PublicKey().ToBase58()] + initiatorAccountsByType, ok := initiatorAccountsByMintAndType[intentMintAccount.PublicKey().ToBase58()] if !ok { - return errors.New("initiator core mint accounts don't exist") + return errors.New("initiator mint accounts don't exist") } initiatorAccounts := make([]*common.AccountRecords, 0) @@ -501,7 +539,7 @@ func (h *SendPublicPaymentIntentHandler) AllowCreation(ctx context.Context, inte // Part 4: Exchange data validation // - if err := validateExchangeDataWithinIntent(ctx, h.data, typedMetadata.ExchangeData); err != nil { + if err := validateExchangeDataWithinIntent(ctx, h.data, typedMetadata.Mint, typedMetadata.ExchangeData); err != nil { return err } @@ -574,7 +612,20 @@ func (h *SendPublicPaymentIntentHandler) validateActions( } // - // Part 2: Check the source and destination accounts are valid + // Part 2: Validate intent and action mints match + // + + intentMint, err := common.GetBackwardsCompatMint(metadata.Mint) + if err != nil { + return err + } + err = validateIntentAndActionMintsMatch(intentMint, actions) + if err != nil { + return err + } + + // + // Part 3: Check the source and destination accounts are valid // sourceAccountRecords, ok := initiatorAccountsByVault[source.PublicKey().ToBase58()] @@ -588,7 +639,12 @@ func (h *SendPublicPaymentIntentHandler) validateActions( return NewIntentValidationError("destination must be a brand new gift card account") } - // Code->Code public ayments can only be made to primary or pool accounts + // Destination mint must match the intent mint + if h.cachedDestinationAccountInfoRecord.MintAccount != intentMint.PublicKey().ToBase58() { + return NewIntentValidationErrorf("destination account is not of %s mint", intentMint.PublicKey().ToBase58()) + } + + // Code->Code public payments can only be made to primary or pool accounts // that are open and managed by Code switch h.cachedDestinationAccountInfoRecord.AccountType { case commonpb.AccountType_PRIMARY, commonpb.AccountType_POOL: @@ -633,7 +689,7 @@ func (h *SendPublicPaymentIntentHandler) validateActions( return NewIntentValidationError("payments to external destinations must be withdrawals") } - // Ensure the destination is the core mint ATA for the client-provided owner, + // Ensure the destination is the intent mint ATA for the client-provided owner, // if provided. We'll check later if this is absolutely required. if metadata.DestinationOwner != nil { destinationOwner, err := common.NewAccountFromProto(metadata.DestinationOwner) @@ -641,7 +697,7 @@ func (h *SendPublicPaymentIntentHandler) validateActions( return err } - ata, err := destinationOwner.ToAssociatedTokenAccount(common.CoreMintAccount) + ata, err := destinationOwner.ToAssociatedTokenAccount(intentMint) if err != nil { return err } @@ -684,11 +740,11 @@ func (h *SendPublicPaymentIntentHandler) validateActions( } // - // Part 3 Validate actions match intent metadata + // Part 4 Validate actions match intent metadata // // - // Part 3.1: Check destination account is paid exact quark amount from the deposit account + // Part 4.1: Check destination account is paid exact quark amount from the deposit account // minus any fees // @@ -714,7 +770,7 @@ func (h *SendPublicPaymentIntentHandler) validateActions( } // - // Part 3.2: Check that the user's deposit account was used as the source of funds + // Part 4.2: Check that the user's deposit account was used as the source of funds // as specified in the metadata // @@ -725,14 +781,14 @@ func (h *SendPublicPaymentIntentHandler) validateActions( return NewActionValidationErrorf(sourceSimulation.Transfers[0].Action, "must send %d quarks from source account", metadata.ExchangeData.Quarks) } - // Part 4: Generic validation of actions that move money + // Part 5: Generic validation of actions that move money - err = validateMoneyMovementActionUserAccounts(intent.SendPublicPayment, initiatorAccountsByVault, actions) + err = validateMoneyMovementActionUserAccounts(ctx, h.data, intent.SendPublicPayment, initiatorAccountsByVault, actions) if err != nil { return err } - // Part 5: Validate open and closed accounts + // Part 6: Validate open and closed accounts if metadata.IsRemoteSend { if len(simResult.GetOpenedAccounts()) != 1 { @@ -813,14 +869,17 @@ func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Cont return errors.New("unexpected metadata proto message") } - giftCardVault, err := common.NewAccountFromPublicKeyBytes(typedProtoMetadata.Source.Value) + mint, err := common.GetBackwardsCompatMint(typedProtoMetadata.Mint) if err != nil { return err } + if !common.IsCoreMint(mint) { + return NewIntentValidationError("only the core mint is supported") + } - usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, currency_util.GetLatestExchangeRateTime()) + giftCardVault, err := common.NewAccountFromPublicKeyBytes(typedProtoMetadata.Source.Value) if err != nil { - return errors.Wrap(err, "error getting current usd exchange rate") + return err } // This is an optimization for payment history. Original fiat amounts are not @@ -834,7 +893,13 @@ func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Cont } h.cachedGiftCardIssuedIntentRecord = giftCardIssuedIntentRecord + usdMarketValue, err := currency_util.CalculateUsdMarketValue(ctx, h.data, mint, typedProtoMetadata.Quarks, currency_util.GetLatestExchangeRateTime()) + if err != nil { + return err + } + intentRecord.IntentType = intent.ReceivePaymentsPublicly + intentRecord.MintAccount = mint.PublicKey().ToBase58() intentRecord.ReceivePaymentsPubliclyMetadata = &intent.ReceivePaymentsPubliclyMetadata{ Source: giftCardVault.PublicKey().ToBase58(), Quantity: typedProtoMetadata.Quarks, @@ -847,7 +912,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Cont OriginalExchangeRate: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.ExchangeRate, OriginalNativeAmount: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.NativeAmount, - UsdMarketValue: usdExchangeRecord.Rate * float64(typedProtoMetadata.Quarks) / float64(common.CoreMintQuarksPerUnit), + UsdMarketValue: usdMarketValue, } return nil @@ -880,6 +945,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) GetBalanceLocks(ctx context.Conte }, nil } +// todo: Not all multi-mint validation checks are implemented func (h *ReceivePaymentsPubliclyIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action) error { typedMetadata := untypedMetadata.GetReceivePaymentsPublicly() if typedMetadata == nil { @@ -988,6 +1054,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) AllowCreation(ctx context.Context // return h.validateActions( + ctx, initiatorAccountsByType, initiatorAccountsByVault, typedMetadata, @@ -997,6 +1064,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) AllowCreation(ctx context.Context } func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( + ctx context.Context, initiatorAccountsByType map[commonpb.AccountType][]*common.AccountRecords, initiatorAccountsByVault map[string]*common.AccountRecords, metadata *transactionpb.ReceivePaymentsPubliclyMetadata, @@ -1008,7 +1076,20 @@ func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( } // - // Part 1: Validate source and destination accounts are valid to use + // Part 1: Validate intent and action mints match + // + + intentMint, err := common.GetBackwardsCompatMint(metadata.Mint) + if err != nil { + return err + } + err = validateIntentAndActionMintsMatch(intentMint, actions) + if err != nil { + return err + } + + // + // Part 2: Validate source and destination accounts are valid to use // // Note: Already validated to be a claimable gift card elsewhere @@ -1025,11 +1106,11 @@ func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( } // - // Part 2: Validate actions match intent + // Part 3: Validate actions match intent // // - // Part 2.1: Check source account pays exact quark amount to destination in a public withdraw + // Part 3.1: Check source account pays exact quark amount to destination in a public withdraw // sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] @@ -1042,7 +1123,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( } // - // Part 2.2: Check destination account is paid exact quark amount from source account in a public withdraw + // Part 3.2: Check destination account is paid exact quark amount from source account in a public withdraw // if destinationSimulation.GetDeltaQuarks(false) != int64(metadata.Quarks) { @@ -1052,7 +1133,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( } // - // Part 3: Validate accounts that are opened and closed + // Part 4: Validate accounts that are opened and closed // if len(simResult.GetOpenedAccounts()) > 0 { @@ -1067,10 +1148,10 @@ func (h *ReceivePaymentsPubliclyIntentHandler) validateActions( } // - // Part 4: Generic validation of actions that move money + // Part 5: Generic validation of actions that move money // - return validateMoneyMovementActionUserAccounts(intent.ReceivePaymentsPublicly, initiatorAccountsByVault, actions) + return validateMoneyMovementActionUserAccounts(ctx, h.data, intent.ReceivePaymentsPublicly, initiatorAccountsByVault, actions) } type PublicDistributionIntentHandler struct { @@ -1104,6 +1185,14 @@ func (h *PublicDistributionIntentHandler) PopulateMetadata(ctx context.Context, return errors.New("unexpected metadata proto message") } + mint, err := common.GetBackwardsCompatMint(typedProtoMetadata.Mint) + if err != nil { + return err + } + if !common.IsCoreMint(mint) { + return NewIntentValidationError("only the core mint is supported") + } + source, err := common.NewAccountFromPublicKeyBytes(typedProtoMetadata.Source.Value) if err != nil { return err @@ -1114,16 +1203,17 @@ func (h *PublicDistributionIntentHandler) PopulateMetadata(ctx context.Context, totalQuarks += distribution.Quarks } - usdExchangeRecord, err := h.data.GetExchangeRate(ctx, currency_lib.USD, currency_util.GetLatestExchangeRateTime()) + usdMarketValue, err := currency_util.CalculateUsdMarketValue(ctx, h.data, mint, totalQuarks, currency_util.GetLatestExchangeRateTime()) if err != nil { - return errors.Wrap(err, "error getting current usd exchange rate") + return err } intentRecord.IntentType = intent.PublicDistribution + intentRecord.MintAccount = mint.PublicKey().ToBase58() intentRecord.PublicDistributionMetadata = &intent.PublicDistributionMetadata{ Source: source.PublicKey().ToBase58(), Quantity: totalQuarks, - UsdMarketValue: usdExchangeRecord.Rate * float64(totalQuarks) / float64(common.CoreMintQuarksPerUnit), + UsdMarketValue: usdMarketValue, } destinationTokenAddresses := make([]string, len(typedProtoMetadata.Distributions)) @@ -1188,7 +1278,7 @@ func (h *PublicDistributionIntentHandler) GetBalanceLocks(ctx context.Context, i }, nil } -// todo: validation against Flipcash through generic interface for pool resolution +// todo: Not all multi-mint validation checks are implemented func (h *PublicDistributionIntentHandler) AllowCreation(ctx context.Context, intentRecord *intent.Record, untypedMetadata *transactionpb.Metadata, actions []*transactionpb.Action) error { typedMetadata := untypedMetadata.GetPublicDistribution() if typedMetadata == nil { @@ -1265,11 +1355,10 @@ func (h *PublicDistributionIntentHandler) AllowCreation(ctx context.Context, int // Part 5: Validate actions // - return h.validateActions(ctx, typedMetadata, actions, simResult) + return h.validateActions(typedMetadata, actions, simResult) } func (h *PublicDistributionIntentHandler) validateActions( - ctx context.Context, metadata *transactionpb.PublicDistributionMetadata, actions []*transactionpb.Action, simResult *LocalSimulationResult, @@ -1279,7 +1368,19 @@ func (h *PublicDistributionIntentHandler) validateActions( } // - // Part 1: Validate source and destination accounts are valid + // Part 1: Validate intent and action mints match + // + intentMint, err := common.GetBackwardsCompatMint(metadata.Mint) + if err != nil { + return err + } + err = validateIntentAndActionMintsMatch(intentMint, actions) + if err != nil { + return err + } + + // + // Part 2: Validate source and destination accounts are valid // // Note: Already validated to be a pool account elsewhere @@ -1316,11 +1417,11 @@ func (h *PublicDistributionIntentHandler) validateActions( } // - // Part 2: Validate actions match intent + // Part 3: Validate actions match intent // // - // Part 2.1: Check source account pays exact quark amount to each destination + // Part 3.1: Check source account pays exact quark amount to each destination // sourceSimulation, ok := simResult.SimulationsByAccount[source.PublicKey().ToBase58()] @@ -1343,7 +1444,7 @@ func (h *PublicDistributionIntentHandler) validateActions( } // - // Part 2.2: Check each destination account is paid exact dstirbution quark amount from source account + // Part 3.2: Check each destination account is paid exact dstirbution quark amount from source account // for i, destination := range destinations { @@ -1364,7 +1465,7 @@ func (h *PublicDistributionIntentHandler) validateActions( } } - // Part 3: Validate open and closed accounts + // Part 4: Validate open and closed accounts if len(simResult.GetOpenedAccounts()) > 0 { return NewIntentValidationError("cannot open any account") @@ -1400,18 +1501,25 @@ func validateAllUserAccountsManagedByCode(ctx context.Context, initiatorAccounts // Other account types (eg. gift cards, external wallets, etc) and intent-specific // complex nuances should be handled elsewhere. func validateMoneyMovementActionUserAccounts( + ctx context.Context, + data code_data.Provider, intentType intent.Type, initiatorAccountsByVault map[string]*common.AccountRecords, actions []*transactionpb.Action, ) error { for _, action := range actions { - var authority, source *common.Account + var mint, 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 + mint, err = common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Mint) + if err != nil { + return err + } + authority, err = common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Authority) if err != nil { return err @@ -1431,6 +1539,11 @@ func validateMoneyMovementActionUserAccounts( // 1. As an auto-return action back to the payer's primary account in a public payment intent for remote send // 2. As a receiver of funds to the primary account in a public receive + mint, err = common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Mint) + if err != nil { + return err + } + authority, err = common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Authority) if err != nil { return err @@ -1456,6 +1569,11 @@ func validateMoneyMovementActionUserAccounts( case *transactionpb.Action_FeePayment: // Fee payments always come from the primary account + mint, err = common.NewAccountFromProto(typedAction.FeePayment.Mint) + if err != nil { + return err + } + authority, err = common.NewAccountFromProto(typedAction.FeePayment.Authority) if err != nil { return err @@ -1474,7 +1592,7 @@ func validateMoneyMovementActionUserAccounts( continue } - expectedTimelockVault, err := getExpectedTimelockVaultFromProtoAccount(authority.ToProto()) + expectedTimelockVault, err := getExpectedTimelockVaultFromProtoAccounts(ctx, data, authority.ToProto(), mint.ToProto()) if err != nil { return err } else if !bytes.Equal(expectedTimelockVault.PublicKey().ToBytes(), source.PublicKey().ToBytes()) { @@ -1523,7 +1641,7 @@ func validateGiftCardAccountOpened( return NewActionValidationError(openAction, "index must be 0") } - derivedVaultAccount, err := getExpectedTimelockVaultFromProtoAccount(openAction.GetOpenAccount().Authority) + derivedVaultAccount, err := getExpectedTimelockVaultFromProtoAccounts(ctx, data, openAction.GetOpenAccount().Authority, openAction.GetOpenAccount().Mint) if err != nil { return err } @@ -1553,7 +1671,21 @@ func validateExternalTokenAccountWithinIntent(ctx context.Context, data code_dat return nil } -func validateExchangeDataWithinIntent(ctx context.Context, data code_data.Provider, proto *transactionpb.ExchangeData) error { +func validateExchangeDataWithinIntent(ctx context.Context, data code_data.Provider, intentMint *commonpb.SolanaAccountId, proto *transactionpb.ExchangeData) error { + intentMintAccount, err := common.GetBackwardsCompatMint(intentMint) + if err != nil { + return err + } + + exchangeMintAccount, err := common.GetBackwardsCompatMint(proto.Mint) + if err != nil { + return err + } + + if !bytes.Equal(intentMintAccount.PublicKey().ToBytes(), exchangeMintAccount.PublicKey().ToBytes()) { + return NewIntentValidationErrorf("expected exchange data mint to be %s", intentMintAccount.PublicKey().ToBase58()) + } + isValid, message, err := currency_util.ValidateClientExchangeData(ctx, data, proto) if err != nil { return err @@ -1604,8 +1736,19 @@ func validateFeePayments( return nil } feePayment := feePayments[0] + feePaymentAction := feePayment.Action.GetFeePayment() + + mintAccount, err := common.GetBackwardsCompatMint(feePaymentAction.Mint) + if err != nil { + return err + } + + // todo: Probably not always going to be the case, but add a strict validation to start + if !common.IsCoreMint(mintAccount) { + return NewActionValidationError(feePayment.Action, "fee payment must be made in core mint") + } - if feePayment.Action.GetFeePayment().Type != expectedFeeType { + if feePaymentAction.Type != expectedFeeType { return NewActionValidationErrorf(feePayment.Action, "expected a %s fee payment", expectedFeeType.String()) } @@ -1624,7 +1767,7 @@ func validateFeePayments( feeAmount = -feeAmount // Because it's coming out of a user account in this simulation var foundUsdExchangeRecord bool - usdExchangeRecords, err := currency_util.GetPotentialClientExchangeRates(ctx, data, currency_lib.USD) + usdExchangeRecords, err := currency_util.GetPotentialClientCoreMintExchangeRates(ctx, data, currency_lib.USD) if err != nil { return err } @@ -1770,12 +1913,22 @@ func validateDistributedPool(ctx context.Context, data code_data.Provider, poolV } func validateTimelockUnlockStateDoesntExist(ctx context.Context, data code_data.Provider, openAction *transactionpb.OpenAccountAction) error { + mintAccount, err := common.NewAccountFromProto(openAction.Mint) + if err != nil { + return err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, data, mintAccount) + if err != nil { + return err + } + authorityAccount, err := common.NewAccountFromProto(openAction.Authority) if err != nil { return err } - timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + timelockAccounts, err := authorityAccount.GetTimelockAccounts(vmConfig) if err != nil { return err } @@ -1791,13 +1944,50 @@ func validateTimelockUnlockStateDoesntExist(ctx context.Context, data code_data. } } -func getExpectedTimelockVaultFromProtoAccount(authorityProto *commonpb.SolanaAccountId) (*common.Account, error) { +func validateIntentAndActionMintsMatch(intentMint *common.Account, actions []*transactionpb.Action) error { + for _, action := range actions { + var actionMint *common.Account + var err error + switch typed := action.Type.(type) { + case *transactionpb.Action_OpenAccount: + actionMint, err = common.GetBackwardsCompatMint(typed.OpenAccount.Mint) + case *transactionpb.Action_NoPrivacyTransfer: + actionMint, err = common.GetBackwardsCompatMint(typed.NoPrivacyTransfer.Mint) + case *transactionpb.Action_NoPrivacyWithdraw: + actionMint, err = common.GetBackwardsCompatMint(typed.NoPrivacyWithdraw.Mint) + case *transactionpb.Action_FeePayment: + actionMint, err = common.GetBackwardsCompatMint(typed.FeePayment.Mint) + default: + return errors.New("unsupported action for mint extraction") + } + + if err != nil { + return err + } + if !bytes.Equal(intentMint.PublicKey().ToBytes(), actionMint.PublicKey().ToBytes()) { + return NewActionValidationErrorf(action, "mint must be %s", intentMint.PublicKey().ToBase58()) + } + } + return nil +} + +func getExpectedTimelockVaultFromProtoAccounts(ctx context.Context, data code_data.Provider, authorityProto, mintProto *commonpb.SolanaAccountId) (*common.Account, error) { + mintAccount, err := common.NewAccountFromProto(mintProto) + if err != nil { + return nil, err + } + + vmConfig, err := common.GetVmConfigForMint(ctx, data, mintAccount) + if err != nil { + return nil, err + } + authorityAccount, err := common.NewAccountFromProto(authorityProto) if err != nil { return nil, err } - timelockAccounts, err := authorityAccount.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + timelockAccounts, err := authorityAccount.GetTimelockAccounts(vmConfig) if err != nil { return nil, err } diff --git a/pkg/code/server/transaction/limits.go b/pkg/code/server/transaction/limits.go index 3a0be50a..49647837 100644 --- a/pkg/code/server/transaction/limits.go +++ b/pkg/code/server/transaction/limits.go @@ -76,15 +76,6 @@ func (s *transactionServer) GetLimits(ctx context.Context, req *transactionpb.Ge } } - usdSendLimits := sendLimits[string(currency_lib.USD)] - - // Inject a core mint limit based on the remaining USD amount and rate - sendLimits[string(common.CoreMintSymbol)] = &transactionpb.SendLimit{ - NextTransaction: usdSendLimits.NextTransaction / float32(usdRate), - MaxPerTransaction: usdSendLimits.MaxPerTransaction / float32(usdRate), - MaxPerDay: usdSendLimits.MaxPerDay / float32(usdRate), - } - return &transactionpb.GetLimitsResponse{ Result: transactionpb.GetLimitsResponse_OK, SendLimitsByCurrency: sendLimits, diff --git a/pkg/code/server/transaction/local_simulation.go b/pkg/code/server/transaction/local_simulation.go index 2f1603ea..f8bf59d0 100644 --- a/pkg/code/server/transaction/local_simulation.go +++ b/pkg/code/server/transaction/local_simulation.go @@ -1,6 +1,7 @@ package transaction_v2 import ( + "bytes" "context" "errors" @@ -9,6 +10,7 @@ import ( "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/timelock" ) @@ -18,6 +20,7 @@ type LocalSimulationResult struct { type TokenAccountSimulation struct { TokenAccount *common.Account + MintAccount *common.Account Transfers []TransferSimulation @@ -39,20 +42,27 @@ type TransferSimulation struct { } // LocalSimulation simulates actions as if they were executed on the blockchain -// taking into account cached Code DB state. +// taking into account cached Code DB state. External state is not considered +// and must be validated elsewhere. func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*transactionpb.Action) (*LocalSimulationResult, error) { result := &LocalSimulationResult{ SimulationsByAccount: make(map[string]TokenAccountSimulation), } + validatedDestinations := make(map[string]any) for _, action := range actions { - var authority, derivedTimelockVault *common.Account + var authority, mint, derivedTimelockVault, destination *common.Account var simulations []TokenAccountSimulation var err error // Simulate the action switch typedAction := action.Type.(type) { case *transactionpb.Action_OpenAccount: + mint, err = common.GetBackwardsCompatMint(typedAction.OpenAccount.Mint) + if err != nil { + return nil, err + } + opened, err := common.NewAccountFromProto(typedAction.OpenAccount.Token) if err != nil { return nil, err @@ -82,11 +92,17 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr simulations, TokenAccountSimulation{ TokenAccount: opened, + MintAccount: mint, Opened: true, OpenAction: action, }, ) case *transactionpb.Action_NoPrivacyTransfer: + mint, err = common.GetBackwardsCompatMint(typedAction.NoPrivacyTransfer.Mint) + if err != nil { + return nil, err + } + source, err := common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Source) if err != nil { return nil, err @@ -98,7 +114,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr return nil, err } - destination, err := common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Destination) + destination, err = common.NewAccountFromProto(typedAction.NoPrivacyTransfer.Destination) if err != nil { return nil, err } @@ -109,6 +125,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr simulations, TokenAccountSimulation{ TokenAccount: source, + MintAccount: mint, Transfers: []TransferSimulation{ { Action: action, @@ -118,6 +135,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr }, TokenAccountSimulation{ TokenAccount: destination, + MintAccount: mint, Transfers: []TransferSimulation{ { Action: action, @@ -127,6 +145,11 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr }, ) case *transactionpb.Action_FeePayment: + mint, err = common.GetBackwardsCompatMint(typedAction.FeePayment.Mint) + if err != nil { + return nil, err + } + source, err := common.NewAccountFromProto(typedAction.FeePayment.Source) if err != nil { return nil, err @@ -144,6 +167,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr simulations, TokenAccountSimulation{ TokenAccount: source, + MintAccount: mint, Transfers: []TransferSimulation{ { Action: action, @@ -155,6 +179,11 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr // todo: Doesn't specify destination, but that's not required yet ) case *transactionpb.Action_NoPrivacyWithdraw: + mint, err = common.GetBackwardsCompatMint(typedAction.NoPrivacyWithdraw.Mint) + if err != nil { + return nil, err + } + source, err := common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Source) if err != nil { return nil, err @@ -166,7 +195,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr return nil, err } - destination, err := common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Destination) + destination, err = common.NewAccountFromProto(typedAction.NoPrivacyWithdraw.Destination) if err != nil { return nil, err } @@ -181,6 +210,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr simulations, TokenAccountSimulation{ TokenAccount: source, + MintAccount: mint, Transfers: []TransferSimulation{ { Action: action, @@ -195,6 +225,7 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr }, TokenAccountSimulation{ TokenAccount: destination, + MintAccount: mint, Transfers: []TransferSimulation{ { Action: action, @@ -210,7 +241,11 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr } // Validate authorities and respective derived timelock vault accounts match. - timelockAccounts, err := authority.GetTimelockAccounts(common.CodeVmAccount, common.CoreMintAccount) + vmConfig, err := common.GetVmConfigForMint(ctx, data, mint) + if err != nil { + return nil, err + } + timelockAccounts, err := authority.GetTimelockAccounts(vmConfig) if err != nil { return nil, err } @@ -218,6 +253,23 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr return nil, NewActionValidationErrorf(action, "token must be %s", timelockAccounts.Vault.PublicKey().ToBase58()) } + // Validate destination accounts make sense given cached state + if destination != nil { + if _, ok := validatedDestinations[destination.PublicKey().ToBase58()]; !ok { + destinationAccountInfoRecord, err := data.GetAccountInfoByTokenAddress(ctx, destination.PublicKey().ToBase58()) + switch err { + case nil: + if mint.PublicKey().ToBase58() != destinationAccountInfoRecord.MintAccount { + return nil, NewActionValidationErrorf(action, "%s mint is invalid", destination.PublicKey().ToBase58()) + } + case account.ErrAccountInfoNotFound: + default: + return nil, err + } + validatedDestinations[destination.PublicKey().ToBase58()] = true + } + } + // Combine the simulated action to all previously simulated actions with // some basic level of validation. for _, simulation := range simulations { @@ -232,27 +284,32 @@ func LocalSimulation(ctx context.Context, data code_data.Provider, actions []*tr if ok { // Attempt to open an already closed account, which isn't supported if combined.Closed && simulation.Opened { - return nil, NewActionValidationError(action, "account cannot be reopened") + return nil, NewActionValidationErrorf(action, "%s cannot be reopened", simulation.TokenAccount.PublicKey().ToBase58()) } // Attempt to open an already opened account if combined.Opened && simulation.Opened { - return nil, NewActionValidationError(action, "account is already opened in another action") + return nil, NewActionValidationErrorf(action, "%s is already opened in another action", simulation.TokenAccount.PublicKey().ToBase58()) } // Funds transferred to an account before it was opened if len(combined.Transfers) > 0 && simulation.Opened { - return nil, NewActionValidationError(action, "opened an account after transferring funds to it") + return nil, NewActionValidationErrorf(action, "opened %s after transferring funds to it", simulation.TokenAccount.PublicKey().ToBase58()) } // Attempt to close an already closed account if combined.Closed && simulation.Closed { - return nil, NewActionValidationError(action, "account is already closed in another action") + return nil, NewActionValidationErrorf(action, "%s is already closed in another action", simulation.TokenAccount.PublicKey().ToBase58()) } // Attempt to send/receive funds to a closed account if combined.Closed && len(simulation.Transfers) > 0 { - return nil, NewActionValidationError(action, "account is closed and cannot send/receive funds") + return nil, NewActionValidationErrorf(action, "%s is closed and cannot send/receive funds", simulation.TokenAccount.PublicKey().ToBase58()) + } + + // Mint changed + if !bytes.Equal(combined.MintAccount.PublicKey().ToBytes(), simulation.MintAccount.PublicKey().ToBytes()) { + return nil, NewActionValidationErrorf(action, "%s mint is invalid", simulation.TokenAccount.PublicKey().ToBase58()) } combined.Transfers = append(combined.Transfers, simulation.Transfers...) @@ -327,7 +384,7 @@ func (s TokenAccountSimulation) EnforceBalances(ctx context.Context, data code_d for _, transfer := range s.Transfers { newBalance = newBalance + transfer.DeltaQuarks if newBalance < 0 { - return NewActionValidationError(transfer.Action, "insufficient balance to perform action") + return NewActionValidationErrorf(transfer.Action, "%s has insufficient balance to perform action", s.TokenAccount.PublicKey().ToBase58()) } // If it's withdrawn out of this account, remove any remaining balance. @@ -339,7 +396,7 @@ func (s TokenAccountSimulation) EnforceBalances(ctx context.Context, data code_d } if s.Closed && newBalance != 0 { - return NewActionValidationError(s.CloseAction, "attempt to close an account with a non-zero balance") + return NewActionValidationErrorf(s.CloseAction, "attempt to close %s with a non-zero balance", s.TokenAccount.PublicKey().ToBase58()) } return nil diff --git a/pkg/code/server/transaction/server.go b/pkg/code/server/transaction/server.go index a1094610..559d5817 100644 --- a/pkg/code/server/transaction/server.go +++ b/pkg/code/server/transaction/server.go @@ -2,6 +2,7 @@ package transaction_v2 import ( "context" + "errors" "sync" "github.com/sirupsen/logrus" @@ -31,7 +32,7 @@ type transactionServer struct { antispamGuard *antispam.Guard amlGuard *aml.Guard - noncePool *transaction.LocalNoncePool + noncePools []*transaction.LocalNoncePool localAccountLocksMu sync.Mutex localAccountLocks map[string]*sync.Mutex @@ -50,17 +51,20 @@ func NewTransactionServer( airdropIntegration AirdropIntegration, antispamGuard *antispam.Guard, amlGuard *aml.Guard, - noncePool *transaction.LocalNoncePool, + noncePools []*transaction.LocalNoncePool, configProvider ConfigProvider, ) (transactionpb.TransactionServer, error) { - var err error - ctx := context.Background() conf := configProvider() - if err := noncePool.Validate(nonce.EnvironmentCvm, common.CodeVmAccount.PublicKey().ToBase58(), nonce.PurposeClientTransaction); err != nil { - return nil, err + _, err := transaction.SelectNoncePool( + nonce.EnvironmentCvm, + common.CodeVmAccount.PublicKey().ToBase58(), + nonce.PurposeClientTransaction, + ) + if err != nil { + return nil, errors.New("nonce pool for core mint is not provided") } s := &transactionServer{ @@ -77,7 +81,7 @@ func NewTransactionServer( antispamGuard: antispamGuard, amlGuard: amlGuard, - noncePool: noncePool, + noncePools: noncePools, localAccountLocks: make(map[string]*sync.Mutex), } diff --git a/pkg/code/transaction/instruction.go b/pkg/code/transaction/instruction.go index c4167535..0eba2181 100644 --- a/pkg/code/transaction/instruction.go +++ b/pkg/code/transaction/instruction.go @@ -6,9 +6,9 @@ import ( "github.com/code-payments/code-server/pkg/solana/system" ) -func makeAdvanceNonceInstruction(nonce *common.Account) (solana.Instruction, error) { +func makeAdvanceNonceInstruction(nonce, authority *common.Account) (solana.Instruction, error) { return system.AdvanceNonce( nonce.PublicKey().ToBytes(), - common.GetSubsidizer().PublicKey().ToBytes(), + authority.PublicKey().ToBytes(), ), nil } diff --git a/pkg/code/transaction/nonce_pool.go b/pkg/code/transaction/nonce_pool.go index fb7e5968..2f9e9842 100644 --- a/pkg/code/transaction/nonce_pool.go +++ b/pkg/code/transaction/nonce_pool.go @@ -27,6 +27,7 @@ const ( ) var ( + ErrNoncePoolNotFound = errors.New("nonce pool not found") ErrNoAvailableNonces = errors.New("no available nonces") ErrNoncePoolClosed = errors.New("nonce pool is closed") ) @@ -223,6 +224,19 @@ func (n *Nonce) ReleaseIfNotReserved(ctx context.Context) { n.pool.mu.Unlock() } +// SelectNoncePool selects a nonce pool from the provided set that matches the +// desired environment and pool type. +// +// ErrNoncePoolNotFound is returned if no nonce pool matches the desired config. +func SelectNoncePool(env nonce.Environment, envInstance string, poolType nonce.Purpose, pools ...*LocalNoncePool) (*LocalNoncePool, error) { + for _, pool := range pools { + if pool.env == env && pool.envInstance == envInstance && pool.poolType == poolType { + return pool, nil + } + } + return nil, ErrNoncePoolNotFound +} + // LocalNoncePool is a pool of nonces that are cached in memory for // quick access. The LocalNoncePool will continually monitor the pool // to ensure sufficient size, as well refresh nonce expiration diff --git a/pkg/code/transaction/transaction.go b/pkg/code/transaction/transaction.go index 64429151..4759fcbf 100644 --- a/pkg/code/transaction/transaction.go +++ b/pkg/code/transaction/transaction.go @@ -19,34 +19,41 @@ import ( // 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) { +func MakeNoncedTransaction(nonce *Nonce, instructions ...solana.Instruction) (solana.Transaction, error) { if len(instructions) == 0 { return solana.Transaction{}, errors.New("no instructions provided") } - advanceNonceInstruction, err := makeAdvanceNonceInstruction(nonce) + payer := common.GetSubsidizer() // todo: Should this be the VM authority? + + advanceNonceInstruction, err := makeAdvanceNonceInstruction(nonce.Account, payer) if err != nil { return solana.Transaction{}, err } instructions = append([]solana.Instruction{advanceNonceInstruction}, instructions...) - txn := solana.NewTransaction(common.GetSubsidizer().PublicKey().ToBytes(), instructions...) - txn.SetBlockhash(bh) + txn := solana.NewTransaction(payer.PublicKey().ToBytes(), instructions...) + txn.SetBlockhash(nonce.Blockhash) return txn, nil } func MakeOpenAccountTransaction( - nonce *common.Account, - bh solana.Blockhash, + nonce *Nonce, + + vmConfig *common.VmConfig, memory *common.Account, accountIndex uint16, timelockAccounts *common.TimelockAccounts, ) (solana.Transaction, error) { - initializeInstruction, err := timelockAccounts.GetInitializeInstruction(memory, accountIndex) + if !bytes.Equal(vmConfig.Vm.PublicKey().ToBytes(), timelockAccounts.Vm.PublicKey().ToBytes()) { + return solana.Transaction{}, errors.New("vm mismatch") + } + + initializeInstruction, err := timelockAccounts.GetInitializeInstruction(vmConfig.Authority, memory, accountIndex) if err != nil { return solana.Transaction{}, err } @@ -56,29 +63,31 @@ func MakeOpenAccountTransaction( compute_budget.SetComputeUnitLimit(50_000), initializeInstruction, } - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, instructions...) } func MakeCompressAccountTransaction( - nonce *common.Account, - bh solana.Blockhash, + nonce *Nonce, + + vmConfig *common.VmConfig, - vm *common.Account, memory *common.Account, accountIndex uint16, + storage *common.Account, + virtualAccountState []byte, ) (solana.Transaction, error) { hasher := sha256.New() hasher.Write(virtualAccountState) hashedVirtualAccountState := hasher.Sum(nil) - signature := ed25519.Sign(common.GetSubsidizer().PrivateKey().ToBytes(), hashedVirtualAccountState) + signature := ed25519.Sign(vmConfig.Authority.PrivateKey().ToBytes(), hashedVirtualAccountState) compressInstruction := cvm.NewCompressInstruction( &cvm.CompressInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), + VmAuthority: vmConfig.Authority.PublicKey().ToBytes(), + Vm: vmConfig.Vm.PublicKey().ToBytes(), VmMemory: memory.PublicKey().ToBytes(), VmStorage: storage.PublicKey().ToBytes(), }, @@ -93,20 +102,22 @@ func MakeCompressAccountTransaction( compute_budget.SetComputeUnitLimit(200_000), compressInstruction, } - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, instructions...) } func MakeInternalWithdrawTransaction( - nonce *common.Account, - bh solana.Blockhash, + nonce *Nonce, + + vmConfig *common.VmConfig, virtualSignature solana.Signature, - vm *common.Account, nonceMemory *common.Account, nonceIndex uint16, + sourceMemory *common.Account, sourceIndex uint16, + destinationMemory *common.Account, destinationIndex uint16, ) (solana.Transaction, error) { @@ -121,8 +132,8 @@ func MakeInternalWithdrawTransaction( execInstruction := cvm.NewExecInstruction( &cvm.ExecInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), + VmAuthority: vmConfig.Authority.PublicKey().ToBytes(), + Vm: vmConfig.Vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, VmMemC: mergedMemoryBanks.C, @@ -140,20 +151,19 @@ func MakeInternalWithdrawTransaction( compute_budget.SetComputeUnitLimit(100_000), execInstruction, } - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, instructions...) } func MakeExternalWithdrawTransaction( - nonce *common.Account, - bh solana.Blockhash, + nonce *Nonce, - virtualSignature solana.Signature, + vmConfig *common.VmConfig, - vm *common.Account, - vmOmnibus *common.Account, + virtualSignature solana.Signature, nonceMemory *common.Account, nonceIndex uint16, + sourceMemory *common.Account, sourceIndex uint16, @@ -164,7 +174,7 @@ func MakeExternalWithdrawTransaction( return solana.Transaction{}, err } - vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) + vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmConfig.Omnibus.PublicKey().ToBytes()) externalAddressPublicKeyBytes := ed25519.PublicKey(externalDestination.PublicKey().ToBytes()) @@ -174,8 +184,8 @@ func MakeExternalWithdrawTransaction( execInstruction := cvm.NewExecInstruction( &cvm.ExecInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), + VmAuthority: vmConfig.Authority.PublicKey().ToBytes(), + Vm: vmConfig.Vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, VmOmnibus: &vmOmnibusPublicKeyBytes, @@ -194,24 +204,26 @@ func MakeExternalWithdrawTransaction( compute_budget.SetComputeUnitLimit(100_000), execInstruction, } - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, instructions...) } func MakeInternalTransferWithAuthorityTransaction( - nonce *common.Account, - bh solana.Blockhash, + nonce *Nonce, + + vmConfig *common.VmConfig, virtualSignature solana.Signature, - vm *common.Account, nonceMemory *common.Account, nonceIndex uint16, + sourceMemory *common.Account, sourceIndex uint16, + destinationMemory *common.Account, destinationIndex uint16, - coreMintQuarks uint64, + quarks uint64, ) (solana.Transaction, error) { mergedMemoryBanks, err := MergeMemoryBanks(nonceMemory, sourceMemory, destinationMemory) if err != nil { @@ -219,14 +231,14 @@ func MakeInternalTransferWithAuthorityTransaction( } vixn := cvm.NewTransferVirtualInstruction(&cvm.TransferVirtualInstructionArgs{ - Amount: coreMintQuarks, + Amount: quarks, Signature: cvm.Signature(virtualSignature), }) execInstruction := cvm.NewExecInstruction( &cvm.ExecInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), + VmAuthority: vmConfig.Authority.PublicKey().ToBytes(), + Vm: vmConfig.Vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, VmMemC: mergedMemoryBanks.C, @@ -244,28 +256,28 @@ func MakeInternalTransferWithAuthorityTransaction( compute_budget.SetComputeUnitLimit(100_000), execInstruction, } - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, instructions...) } func MakeExternalTransferWithAuthorityTransaction( - nonce *common.Account, - bh solana.Blockhash, + nonce *Nonce, - virtualSignature solana.Signature, + vmConfig *common.VmConfig, - vm *common.Account, - vmOmnibus *common.Account, + virtualSignature solana.Signature, nonceMemory *common.Account, nonceIndex uint16, + sourceMemory *common.Account, sourceIndex uint16, - isCreateOnSend bool, externalDestinationOwner *common.Account, externalDestination *common.Account, - coreMintQuarks uint64, + isCreateOnSend bool, + mint *common.Account, + quarks uint64, ) (solana.Transaction, error) { mergedMemoryBanks, err := MergeMemoryBanks(nonceMemory, sourceMemory) if err != nil { @@ -274,17 +286,17 @@ func MakeExternalTransferWithAuthorityTransaction( externalAddressPublicKeyBytes := ed25519.PublicKey(externalDestination.PublicKey().ToBytes()) - vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmOmnibus.PublicKey().ToBytes()) + vmOmnibusPublicKeyBytes := ed25519.PublicKey(vmConfig.Omnibus.PublicKey().ToBytes()) vixn := cvm.NewExternalTransferVirtualInstruction(&cvm.TransferVirtualInstructionArgs{ - Amount: coreMintQuarks, + Amount: quarks, Signature: cvm.Signature(virtualSignature), }) execInstruction := cvm.NewExecInstruction( &cvm.ExecInstructionAccounts{ - VmAuthority: common.GetSubsidizer().PublicKey().ToBytes(), - Vm: vm.PublicKey().ToBytes(), + VmAuthority: vmConfig.Authority.PublicKey().ToBytes(), + Vm: vmConfig.Vm.PublicKey().ToBytes(), VmMemA: mergedMemoryBanks.A, VmMemB: mergedMemoryBanks.B, VmOmnibus: &vmOmnibusPublicKeyBytes, @@ -313,9 +325,9 @@ func MakeExternalTransferWithAuthorityTransaction( } createIdempotentInstruction, ata, err := token.CreateAssociatedTokenAccountIdempotent( - common.GetSubsidizer().PublicKey().ToBytes(), + common.GetSubsidizer().PublicKey().ToBytes(), // todo: Should this be the VM authority? externalDestinationOwner.PublicKey().ToBytes(), - common.CoreMintAccount.PublicKey().ToBytes(), + mint.PublicKey().ToBytes(), ) if err != nil { return solana.Transaction{}, err @@ -326,7 +338,7 @@ func MakeExternalTransferWithAuthorityTransaction( instructions = append(instructions, createIdempotentInstruction) } instructions = append(instructions, execInstruction) - return MakeNoncedTransaction(nonce, bh, instructions...) + return MakeNoncedTransaction(nonce, instructions...) } type MergedMemoryBankResult struct { diff --git a/pkg/code/transaction/transaction_test.go b/pkg/code/transaction/transaction_test.go index 7371f069..1e9795d1 100644 --- a/pkg/code/transaction/transaction_test.go +++ b/pkg/code/transaction/transaction_test.go @@ -26,6 +26,11 @@ func TestTransaction_MakeNoncedTransaction_HappyPath(t *testing.T) { var typedBlockhash solana.Blockhash copy(typedBlockhash[:], untypedBlockhash) + nonce := &Nonce{ + Account: nonceAccount, + Blockhash: typedBlockhash, + } + ixns := []solana.Instruction{ token.Transfer( testutil.NewRandomAccount(t).PublicKey().ToBytes(), @@ -46,7 +51,7 @@ func TestTransaction_MakeNoncedTransaction_HappyPath(t *testing.T) { ), } - txn, err := MakeNoncedTransaction(nonceAccount, typedBlockhash, ixns...) + txn, err := MakeNoncedTransaction(nonce, ixns...) require.NoError(t, err) assert.Equal(t, typedBlockhash, txn.Message.RecentBlockhash) @@ -75,7 +80,12 @@ func TestTransaction_MakeNoncedTransaction_NoInstructions(t *testing.T) { var typedBlockhash solana.Blockhash copy(typedBlockhash[:], untypedBlockhash) - _, err = MakeNoncedTransaction(nonceAccount, typedBlockhash) + nonce := &Nonce{ + Account: nonceAccount, + Blockhash: typedBlockhash, + } + + _, err = MakeNoncedTransaction(nonce) assert.Error(t, err) } diff --git a/pkg/currency/iso.go b/pkg/currency/iso.go index 3da242ab..a2a7a118 100644 --- a/pkg/currency/iso.go +++ b/pkg/currency/iso.go @@ -3,10 +3,6 @@ package currency type Code string const ( - USDC Code = "usdc" - USDG Code = "usdg" - USDT Code = "usdt" - AED Code = "aed" AFN Code = "afn" ALL Code = "all" diff --git a/pkg/testutil/vm.go b/pkg/testutil/vm.go new file mode 100644 index 00000000..6770877c --- /dev/null +++ b/pkg/testutil/vm.go @@ -0,0 +1,24 @@ +package testutil + +import ( + "testing" + + "github.com/code-payments/code-server/pkg/code/common" +) + +func NewRandomVmConfig(t *testing.T, isCore bool) *common.VmConfig { + if isCore { + return &common.VmConfig{ + Authority: common.GetSubsidizer(), + Vm: common.CodeVmAccount, + Omnibus: common.CodeVmOmnibusAccount, + Mint: common.CoreMintAccount, + } + } + return &common.VmConfig{ + Authority: NewRandomAccount(t), + Vm: NewRandomAccount(t), + Omnibus: NewRandomAccount(t), + Mint: NewRandomAccount(t), + } +}