diff --git a/config/config.go b/config/config.go index 1a44d2a..6f81fa7 100644 --- a/config/config.go +++ b/config/config.go @@ -7,41 +7,54 @@ import ( ) const ( - createAccountPath = "/api/v2/account/create" - trackStatusPath = "/api/track" - balancePathPrefix = "/api/account" - trackPath = "/api/v2/account/status" - tokenTransferPrefix = "/api/v2/token/transfer" - voucherHoldingsPathPrefix = "/api/v1/holdings" - voucherTransfersPathPrefix = "/api/v1/transfers/last10" - voucherDataPathPrefix = "/api/v1/token" - AliasPrefix = "api/v1/alias" - SendSMSPrefix = "api/v1/external/upsell" - AliasEnsPrefix = "/api/v1/bypass" - ExternalSMSPrefix = "/api/v1/external" + createAccountPath = "/api/v2/account/create" + trackStatusPath = "/api/track" + balancePathPrefix = "/api/account" + trackPath = "/api/v2/account/status" + tokenTransferPrefix = "/api/v2/token/transfer" + voucherHoldingsPathPrefix = "/api/v1/holdings" + voucherTransfersPathPrefix = "/api/v1/transfers/last10" + voucherDataPathPrefix = "/api/v1/token" + SendSMSPrefix = "api/v1/external/upsell" + poolDepositPrefix = "/api/v2/pool/deposit" + poolSwapQoutePrefix = "/api/v2/pool/quote" + poolSwapPrefix = "/api/v2/pool/swap" + topPoolsPrefix = "/api/v1/pool/top" + retrievePoolDetailsPrefix = "/api/v1/pool/reverse" + poolSwappableVouchersPrefix = "/api/v1/pool" + AliasRegistrationPrefix = "/api/v1/internal/register" + AliasResolverPrefix = "/api/v1/resolve" + ExternalSMSPrefix = "/api/v1/external" ) var ( - custodialURLBase string - dataURLBase string - BearerToken string - aliasEnsURLBase string - externalSMSBase string + custodialURLBase string + dataURLBase string + BearerToken string + aliasEnsURLBase string + externalSMSBase string + IncludeStablesParam string ) var ( - CreateAccountURL string - TrackStatusURL string - BalanceURL string - TrackURL string - TokenTransferURL string - VoucherHoldingsURL string - VoucherTransfersURL string - VoucherDataURL string - CheckAliasURL string - SendSMSURL string - AliasEnsURL string - ExternalSMSURL string + CreateAccountURL string + TrackStatusURL string + BalanceURL string + TrackURL string + TokenTransferURL string + VoucherHoldingsURL string + VoucherTransfersURL string + VoucherDataURL string + PoolDepositURL string + PoolSwapQuoteURL string + PoolSwapURL string + TopPoolsURL string + RetrievePoolDetailsURL string + PoolSwappableVouchersURL string + SendSMSURL string + AliasRegistrationURL string + AliasResolverURL string + ExternalSMSURL string ) func setBase() error { @@ -52,6 +65,7 @@ func setBase() error { aliasEnsURLBase = env.GetEnv("ALIAS_ENS_BASE", "http://localhost:5015") externalSMSBase = env.GetEnv("EXTERNAL_SMS_BASE", "http://localhost:5035") BearerToken = env.GetEnv("BEARER_TOKEN", "") + IncludeStablesParam = env.GetEnv("INCLUDE_STABLES_PARAM", "false") _, err = url.Parse(custodialURLBase) if err != nil { @@ -78,9 +92,15 @@ func LoadConfig() error { VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) - CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix) SendSMSURL, _ = url.JoinPath(dataURLBase, SendSMSPrefix) - AliasEnsURL, _ = url.JoinPath(aliasEnsURLBase, AliasEnsPrefix) + PoolDepositURL, _ = url.JoinPath(custodialURLBase, poolDepositPrefix) + PoolSwapQuoteURL, _ = url.JoinPath(custodialURLBase, poolSwapQoutePrefix) + PoolSwapURL, _ = url.JoinPath(custodialURLBase, poolSwapPrefix) + TopPoolsURL, _ = url.JoinPath(dataURLBase, topPoolsPrefix) + RetrievePoolDetailsURL, _ = url.JoinPath(dataURLBase, retrievePoolDetailsPrefix) + PoolSwappableVouchersURL, _ = url.JoinPath(dataURLBase, poolSwappableVouchersPrefix) + AliasRegistrationURL, _ = url.JoinPath(aliasEnsURLBase, AliasRegistrationPrefix) + AliasResolverURL, _ = url.JoinPath(aliasEnsURLBase, AliasResolverPrefix) ExternalSMSURL, _ = url.JoinPath(externalSMSBase, ExternalSMSPrefix) return nil diff --git a/dev/api.go b/dev/api.go index 8c7ef90..26e1e4c 100644 --- a/dev/api.go +++ b/dev/api.go @@ -34,6 +34,9 @@ const ( defaultDecimals = 6 zeroAddress string = "0x0000000000000000000000000000000000000000" defaultVoucherBalance float64 = 500.00 + cityPoolAddress string = "0x3b517308D858a47458aD5C8E699697C5dc91Da0F" + poolName string = "citypool" + PoolSymbol string = "CTY" ) type Tx struct { @@ -91,6 +94,14 @@ type Voucher struct { Location string `json: "location"` } +type Pool struct { + Name string `json: "name"` + Symbol string `json: "symbol"` + Address string `json: "address"` + Vouchers []Voucher `json: "voucher"` + PoolLimit map[string]string `json: "poollimit"` +} + type DevAccountService struct { db db.Db accounts map[string]Account @@ -106,6 +117,7 @@ type DevAccountService struct { defaultAccount string emitterFunc event.EmitterFunc pfx []byte + pools map[string]Pool } func NewDevAccountService(ctx context.Context, ss storage.StorageService) *DevAccountService { @@ -118,6 +130,7 @@ func NewDevAccountService(ctx context.Context, ss storage.StorageService) *DevAc txs: make(map[string]Tx), txsTrack: make(map[string]string), autoVoucherValue: make(map[string]int), + pools: make(map[string]Pool), defaultAccount: zeroAddress, pfx: []byte("__"), } @@ -171,6 +184,15 @@ func (das *DevAccountService) loadAccount(ctx context.Context, pubKey string, v return nil } +func (p *Pool) hasVoucher(voucherAddress string) bool { + for _, value := range p.Vouchers { + if value.Address == voucherAddress { + return true + } + } + return false +} + func (das *DevAccountService) loadTx(ctx context.Context, hsh string, v []byte) error { var mytx Tx @@ -193,6 +215,17 @@ func (das *DevAccountService) loadAlias(ctx context.Context, alias string, key [ return nil } +func (das *DevAccountService) loadPoolInfo(ctx context.Context, name string, v []byte) error { + var pool Pool + + err := json.Unmarshal(v, &pool) + if err != nil { + return fmt.Errorf("failed to unmarshall pool info: %v", err) + } + das.pools[name] = pool + return nil +} + func (das *DevAccountService) loadItem(ctx context.Context, k []byte, v []byte) error { var err error s := string(k) @@ -209,6 +242,8 @@ func (das *DevAccountService) loadItem(ctx context.Context, k []byte, v []byte) } else if ss[0] == "alias" { err = das.loadAlias(ctx, ss[1], k) logg.ErrorCtxf(ctx, "loading aliases failed", "error_load_aliases", err) + } else if ss[0] == "pool" { + err = das.loadPoolInfo(ctx, ss[1], v) } else { logg.ErrorCtxf(ctx, "unknown double underscore key", "key", ss[0]) } @@ -224,13 +259,13 @@ func (das *DevAccountService) loadAll(ctx context.Context) error { } for true { k, v := dumper.Next(ctx) + logg.InfoCtxf(ctx, "loading all", "key", string(k), "value", string(v)) if k == nil { break } if !bytes.HasPrefix(k, das.pfx) { continue } - err = das.loadItem(ctx, k, v) if err != nil { return err @@ -265,6 +300,35 @@ func (das *DevAccountService) WithAutoVoucher(ctx context.Context, symbol string return das } +func (das *DevAccountService) RegisterPool(ctx context.Context, name string, sm string) error { + var seedVouchers []Voucher + + h := sha1.New() + h.Write([]byte(sm)) + z := h.Sum(nil) + pooladdr := fmt.Sprintf("0x%x", z) + + p := Pool{ + Name: name, + Symbol: sm, + Address: pooladdr, + PoolLimit: make(map[string]string), + } + + for _, v := range das.vouchers { + //pre-load vouchers with vouchers when a pool is registered + seedVouchers = append(seedVouchers, v) + p.PoolLimit[v.Address] = fmt.Sprintf("%f", defaultVoucherBalance) + } + p.Vouchers = append(p.Vouchers, seedVouchers...) + + err := das.savePoolInfo(ctx, p) + if err != nil { + return err + } + return nil +} + // TODO: add persistence for vouchers // TODO: set max balance for 0x00 address func (das *DevAccountService) AddVoucher(ctx context.Context, symbol string) error { @@ -345,6 +409,20 @@ func (das *DevAccountService) saveAccount(ctx context.Context, acc Account) erro return das.db.Put(ctx, []byte(k), v) } +func (das *DevAccountService) savePoolInfo(ctx context.Context, pool Pool) error { + if das.db == nil { + return nil + } + k := das.prefixKeyFor("pool", pool.Address) + v, err := json.Marshal(pool) + if err != nil { + return err + } + das.db.SetSession("") + das.db.SetPrefix(db.DATATYPE_USERDATA) + return das.db.Put(ctx, []byte(k), v) +} + func (das *DevAccountService) saveAlias(ctx context.Context, alias map[string]string) error { if das.db == nil { return fmt.Errorf("Db cannot be nil") @@ -415,6 +493,80 @@ func (das *DevAccountService) CreateAccount(ctx context.Context) (*models.Accoun }, nil } +func (das *DevAccountService) PoolDeposit(ctx context.Context, amount, from, poolAddress, tokenAddress string) (*models.PoolDepositResult, error) { + _, ok := das.accounts[from] + if !ok { + return nil, fmt.Errorf("account not found (publickey): %v", from) + } + sym, ok := das.vouchersAddress[tokenAddress] + if !ok { + return nil, fmt.Errorf("voucher address %v not found", tokenAddress) + } + uid, err := uuid.NewV4() + if err != nil { + return nil, err + } + + _, ok = das.vouchers[sym] + if !ok { + return nil, fmt.Errorf("voucher address %v found but does not resolve", tokenAddress) + } + + if err != nil { + return nil, err + } + return &models.PoolDepositResult{ + TrackingId: uid.String(), + }, nil +} + +func (das *DevAccountService) GetPoolSwapQuote(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapQuoteResult, error) { + _, ok := das.accounts[from] + if !ok { + return nil, fmt.Errorf("account not found (publickey): %v", from) + } + p, ok := das.pools[poolAddress] + if !ok { + return nil, fmt.Errorf("pool address %v not found", poolAddress) + } + + //resolve the token address you are trying to swap from(fromTokenAddress) + ok = p.hasVoucher(fromTokenAddress) + if !ok { + return nil, fmt.Errorf("voucher with address %v not found in the pool", fromTokenAddress) + } + + //Return a a quote that is equal to the amount entered + return &models.PoolSwapQuoteResult{IncludesFeesDeduction: false, OutValue: amount}, nil +} + +func (das *DevAccountService) PoolSwap(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapResult, error) { + uid, err := uuid.NewV4() + if err != nil { + return nil, err + } + + p, ok := das.pools[poolAddress] + if !ok { + return nil, fmt.Errorf("pool address %v not found", toTokenAddress) + } + _, ok = das.accounts[from] + if !ok { + return nil, fmt.Errorf("account not found (publickey): %v", from) + } + ok = p.hasVoucher(fromTokenAddress) + if !ok { + return nil, fmt.Errorf("token %v not found in the pool", fromTokenAddress) + } + + ok = p.hasVoucher(toTokenAddress) + if !ok { + return nil, fmt.Errorf("token %v not found in the pool", toTokenAddress) + } + + return &models.PoolSwapResult{TrackingId: uid.String()}, nil +} + func (das *DevAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { var ok bool _, ok = das.accounts[publicKey] @@ -435,7 +587,7 @@ func (das *DevAccountService) FetchVouchers(ctx context.Context, publicKey strin //TODO: Iterate over the account acc.Balances object for _, voucher := range das.vouchers { holdings = append(holdings, dataserviceapi.TokenHoldings{ - ContractAddress: voucher.Address, + TokenAddress: voucher.Address, TokenSymbol: voucher.Symbol, TokenDecimals: strconv.Itoa(voucher.Decimals), Balance: strconv.Itoa(int(defaultVoucherBalance)), @@ -668,3 +820,83 @@ func (das *DevAccountService) SendPINResetSMS(ctx context.Context, admin, phone func (das *DevAccountService) SendAddressSMS(ctx context.Context, publicKey, originPhone string) error { return fmt.Errorf("unimplemented") } + +func (das *DevAccountService) FetchTopPools(ctx context.Context) ([]dataserviceapi.PoolDetails, error) { + var topPools []dataserviceapi.PoolDetails + for _, p := range das.pools { + topPools = append(topPools, dataserviceapi.PoolDetails{ + PoolName: p.Name, + PoolSymbol: p.Symbol, + PoolContractAdrress: p.Address, + }) + } + return topPools, nil +} + +func (das *DevAccountService) RetrievePoolDetails(ctx context.Context, sym string) (*dataserviceapi.PoolDetails, error) { + testPool := &dataserviceapi.PoolDetails{ + PoolName: "DevTest", + PoolSymbol: "DEVT", + PoolContractAdrress: "0x145F87d6198dEDD45C614FFD8b70E9a2fCCc5cc9", + LimiterAddress: "", + VoucherRegistry: "", + } + + return testPool, nil +} + +func (das *DevAccountService) GetPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + var swapFromList []dataserviceapi.TokenHoldings + + p, ok := das.pools[poolAddress] + if !ok { + return nil, fmt.Errorf("Invalid pool address: %v", poolAddress) + } + for _, v := range p.Vouchers { + swapFromList = append(swapFromList, dataserviceapi.TokenHoldings{ + TokenAddress: v.Address, + TokenSymbol: v.Symbol, + TokenDecimals: string(defaultDecimals), + Balance: fmt.Sprintf("%f", defaultVoucherBalance), + }) + } + + return swapFromList, nil +} + +func (das *DevAccountService) GetPoolSwappableVouchers(ctx context.Context, poolAddress string) ([]dataserviceapi.TokenHoldings, error) { + var swapToList []dataserviceapi.TokenHoldings + _, ok := das.pools[poolAddress] + if !ok { + return nil, fmt.Errorf("Invalid pool address: %v", poolAddress) + } + for _, voucher := range das.vouchers { + swapToList = append(swapToList, dataserviceapi.TokenHoldings{ + TokenAddress: voucher.Address, + TokenSymbol: voucher.Symbol, + TokenDecimals: strconv.Itoa(voucher.Decimals), + }) + } + return swapToList, nil +} + +func (das *DevAccountService) GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) { + p, ok := das.pools[poolAddress] + if !ok { + return nil, fmt.Errorf("Pool address: %v not found ", poolAddress) + } + limit, ok := p.PoolLimit[fromTokenAddress] + if !ok { + return nil, fmt.Errorf("Token address: %v not found in the pool", fromTokenAddress) + } + + return &models.MaxLimitResult{ + Max: limit, + }, nil +} + +func (das *DevAccountService) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) { + return &models.TokenInPoolResult{ + CanSwapFrom: true, + }, nil +} diff --git a/dev/data/swap_from.json b/dev/data/swap_from.json new file mode 100644 index 0000000..e523972 --- /dev/null +++ b/dev/data/swap_from.json @@ -0,0 +1,32 @@ +{ + "ok": true, + "description": "Swap from list", + "result": { + "filtered": [ + { + "contractAddress": "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", + "tokenSymbol": "AMANI", + "tokenDecimals": "6", + "balance": "" + }, + { + "contractAddress": "0xF0C3C7581b8b96B59a97daEc8Bd48247cE078674", + "tokenSymbol": "AMUA", + "tokenDecimals": "6", + "balance": "" + }, + { + "contractAddress": "0x371455a30fc62736145Bd8429Fcc6481186f235F", + "tokenSymbol": "BAHARI", + "tokenDecimals": "6", + "balance": "" + }, + { + "contractAddress": "0x7cA6113b59c24a880F382C7E12d609a6Eb05246b", + "tokenSymbol": "BANGLA", + "tokenDecimals": "6", + "balance": "" + } + ] + } +} \ No newline at end of file diff --git a/dev/data/swap_to.json b/dev/data/swap_to.json new file mode 100644 index 0000000..c8906b6 --- /dev/null +++ b/dev/data/swap_to.json @@ -0,0 +1,14 @@ +{ + "ok": true, + "description": "Swap to list", + "result": { + "filtered": [ + { + "contractAddress": "0x765DE816845861e75A25fCA122bb6898B8B1282a", + "tokenSymbol": "cUSD", + "tokenDecimals": "18", + "balance": "" + } + ] + } +} \ No newline at end of file diff --git a/dev/data/top_pools.json b/dev/data/top_pools.json new file mode 100644 index 0000000..85e120c --- /dev/null +++ b/dev/data/top_pools.json @@ -0,0 +1,43 @@ +{ + "ok": true, + "description": "Top 5 pools sorted by swaps", + "result": { + "topPools": [ + { + "poolName": "Kenya ROLA Pool", + "poolSymbol": "ROLA", + "poolContractAddress": "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e", + "limiterAddress": "", + "voucherRegistry": "" + }, + { + "poolName": "Nairobi ROLA Pool", + "poolSymbol": "NAIROBI", + "poolContractAddress": "0xB0660Ac1Ee3d32ea35bc728D7CA1705Fa5A37528", + "limiterAddress": "", + "voucherRegistry": "" + }, + { + "poolName": "Friends of Kiriba Ecosystem ", + "poolSymbol": "FRIENDS", + "poolContractAddress": "0xC4848263821FA02baB2181910A2eFb9CECb2c21C", + "limiterAddress": "", + "voucherRegistry": "" + }, + { + "poolName": "Resilient Community Waqfs", + "poolSymbol": "REZILIENS", + "poolContractAddress": "0x1e40951d7a28147D8B4A554C60c42766C92e2Fc6", + "limiterAddress": "", + "voucherRegistry": "" + }, + { + "poolName": "GrE Tech", + "poolSymbol": "GRET", + "poolContractAddress": "0xb7B9d0A264eD1a8E2418571B7AC5933C79C9c2B8", + "limiterAddress": "", + "voucherRegistry": "" + } + ] + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 0d62c17..8acb98e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250122123424-6749c632b0a2 github.com/gofrs/uuid v4.4.0+incompatible github.com/grassrootseconomics/eth-custodial v1.3.0-beta - github.com/grassrootseconomics/ussd-data-service v1.2.0-beta + github.com/grassrootseconomics/ussd-data-service v1.6.0-beta github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index c88d977..470c903 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,16 @@ github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQ github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk= github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0= +github.com/grassrootseconomics/ussd-data-service v1.4.0-beta h1:4fMd/3h2ZIhRg4GdHQmRw5FfD3MpJvFNNJQo+Q27f5M= +github.com/grassrootseconomics/ussd-data-service v1.4.0-beta/go.mod h1:9sGnorpKaK76FmOGXoh/xv7x5siSFNYdXxQo9BKW4DI= +github.com/grassrootseconomics/ussd-data-service v1.4.4-beta h1:turlyo0i3OLj29mWpWNoB/3Qao8qEngT/5d1jDWTZE4= +github.com/grassrootseconomics/ussd-data-service v1.4.4-beta/go.mod h1:9sGnorpKaK76FmOGXoh/xv7x5siSFNYdXxQo9BKW4DI= +github.com/grassrootseconomics/ussd-data-service v1.4.7-beta h1:yAe1YaOBsdxW2m20jnVU4F0kLmFr+mK/gHCWEdHmE90= +github.com/grassrootseconomics/ussd-data-service v1.4.7-beta/go.mod h1:9sGnorpKaK76FmOGXoh/xv7x5siSFNYdXxQo9BKW4DI= +github.com/grassrootseconomics/ussd-data-service v1.5.0-beta h1:BSSQL/yPEvTVku9ja/ENZyZdwZkEaiTzzOUfg72bTy4= +github.com/grassrootseconomics/ussd-data-service v1.5.0-beta/go.mod h1:9sGnorpKaK76FmOGXoh/xv7x5siSFNYdXxQo9BKW4DI= +github.com/grassrootseconomics/ussd-data-service v1.6.0-beta h1:pY6zns6ifXyClRxP+JJaWrged3oRE7tTS2xaftF9clA= +github.com/grassrootseconomics/ussd-data-service v1.6.0-beta/go.mod h1:9sGnorpKaK76FmOGXoh/xv7x5siSFNYdXxQo9BKW4DI= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/models/pool_swap_response.go b/models/pool_swap_response.go new file mode 100644 index 0000000..4f2c088 --- /dev/null +++ b/models/pool_swap_response.go @@ -0,0 +1,22 @@ +package models + +type PoolDepositResult struct { + TrackingId string `json:"trackingId"` +} + +type PoolSwapQuoteResult struct { + IncludesFeesDeduction bool `json:"includesFeesDeduction"` + OutValue string `json:"outValue"` +} + +type PoolSwapResult struct { + TrackingId string `json:"trackingId"` +} + +type MaxLimitResult struct { + Max string `json:"max"` +} + +type TokenInPoolResult struct { + CanSwapFrom bool `json:"canSwapFrom"` +} diff --git a/remote/account_service.go b/remote/account_service.go index 3a1b3ab..d9708ef 100644 --- a/remote/account_service.go +++ b/remote/account_service.go @@ -20,4 +20,13 @@ type AccountService interface { SendUpsellSMS(ctx context.Context, inviterPhone, inviteePhone string) (*models.SendSMSResponse, error) SendAddressSMS(ctx context.Context, publicKey, originPhone string) error SendPINResetSMS(ctx context.Context, admin, phone string) error + PoolDeposit(ctx context.Context, amount, from, poolAddress, tokenAddress string) (*models.PoolDepositResult, error) + FetchTopPools(ctx context.Context) ([]dataserviceapi.PoolDetails, error) + RetrievePoolDetails(ctx context.Context, sym string) (*dataserviceapi.PoolDetails, error) + GetPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) + GetPoolSwappableVouchers(ctx context.Context, poolAddress string) ([]dataserviceapi.TokenHoldings, error) + GetPoolSwapQuote(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapQuoteResult, error) + PoolSwap(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapResult, error) + GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) + CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) } diff --git a/remote/http/service.go b/remote/http/service.go index 3810af3..1c41ab7 100644 --- a/remote/http/service.go +++ b/remote/http/service.go @@ -234,35 +234,283 @@ func (as *HTTPAccountService) CheckAliasAddress(ctx context.Context, alias strin } func resolveAliasAddress(ctx context.Context, alias string) (*models.AliasAddress, error) { - var ( - aliasEnsResult models.AliasEnsAddressResult - ) + var aliasEnsResult models.AliasEnsAddressResult - ep, err := url.JoinPath(config.AliasEnsURL, "/resolve") + fullURL, err := url.JoinPath(config.AliasResolverURL, alias) if err != nil { return nil, err } - u, err := url.Parse(ep) + req, err := http.NewRequest("GET", fullURL, nil) if err != nil { return nil, err } - query := u.Query() - query.Set("name", alias) - u.RawQuery = query.Encode() - - req, err := http.NewRequest("GET", u.String(), nil) - if err != nil { - return nil, err - } _, err = doRequest(ctx, req, &aliasEnsResult) if err != nil { return nil, err } - return &models.AliasAddress{Address: aliasEnsResult.Address}, err + + return &models.AliasAddress{Address: aliasEnsResult.Address}, nil } +func (as *HTTPAccountService) FetchTopPools(ctx context.Context) ([]dataserviceapi.PoolDetails, error) { + svc := dev.NewDevAccountService(ctx, as.SS) + if as.UseApi { + return fetchCustodialTopPools(ctx) + } else { + return svc.FetchTopPools(ctx) + } +} + +func fetchCustodialTopPools(ctx context.Context) ([]dataserviceapi.PoolDetails, error) { + var r struct { + TopPools []dataserviceapi.PoolDetails `json:"topPools"` + } + + req, err := http.NewRequest("GET", config.TopPoolsURL, nil) + if err != nil { + return nil, err + } + + _, err = doRequest(ctx, req, &r) + return r.TopPools, nil +} + +func (as *HTTPAccountService) RetrievePoolDetails(ctx context.Context, sym string) (*dataserviceapi.PoolDetails, error) { + if as.UseApi { + return retrievePoolDetails(ctx, sym) + } else { + return nil, nil + } +} + +func retrievePoolDetails(ctx context.Context, sym string) (*dataserviceapi.PoolDetails, error) { + var r struct { + PoolDetails dataserviceapi.PoolDetails `json:"poolDetails"` + } + + ep, err := url.JoinPath(config.RetrievePoolDetailsURL, sym) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r.PoolDetails, nil +} + +func (as *HTTPAccountService) PoolDeposit(ctx context.Context, amount, from, poolAddress, tokenAddress string) (*models.PoolDepositResult, error) { + var r models.PoolDepositResult + + //pool deposit payload + payload := map[string]string{ + "amount": amount, + "from": from, + "poolAddress": poolAddress, + "tokenAddress": tokenAddress, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + return &r, nil +} + +func (as *HTTPAccountService) GetPoolSwapQuote(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapQuoteResult, error) { + var r models.PoolSwapQuoteResult + + //pool swap quote payload + payload := map[string]string{ + "amount": amount, + "from": from, + "fromTokenAddress": fromTokenAddress, + "poolAddress": poolAddress, + "toTokenAddress": toTokenAddress, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", config.PoolSwapQuoteURL, bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + return &r, nil +} + +func (as *HTTPAccountService) GetPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + if as.UseApi { + return as.getPoolSwappableFromVouchers(ctx, poolAddress, publicKey) + } else { + svc := dev.NewDevAccountService(ctx, as.SS) + return svc.GetPoolSwappableFromVouchers(ctx, poolAddress, publicKey) + } + +} + +func (as *HTTPAccountService) getPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + var r struct { + PoolSwappableVouchers []dataserviceapi.TokenHoldings `json:"filtered"` + } + ep, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "from", publicKey) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + + return r.PoolSwappableVouchers, nil +} + +func (as *HTTPAccountService) GetPoolSwappableVouchers(ctx context.Context, poolAddress string) ([]dataserviceapi.TokenHoldings, error) { + svc := dev.NewDevAccountService(ctx, as.SS) + if as.UseApi { + return as.getPoolSwappableVouchers(ctx, poolAddress) + } else { + return svc.GetPoolSwappableVouchers(ctx, poolAddress) + } +} + +func (as HTTPAccountService) getPoolSwappableVouchers(ctx context.Context, poolAddress string) ([]dataserviceapi.TokenHoldings, error) { + var r struct { + PoolSwappableVouchers []dataserviceapi.TokenHoldings `json:"filtered"` + } + + basePath, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "to") + if err != nil { + return nil, err + } + + parsedURL, err := url.Parse(basePath) + if err != nil { + return nil, err + } + + query := parsedURL.Query() + if config.IncludeStablesParam != "" { + query.Set("stables", config.IncludeStablesParam) + } + parsedURL.RawQuery = query.Encode() + + req, err := http.NewRequest("GET", parsedURL.String(), nil) + if err != nil { + return nil, err + } + + _, err = doRequest(ctx, req, &r) + return r.PoolSwappableVouchers, nil +} + +func (as *HTTPAccountService) PoolSwap(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapResult, error) { + var r models.PoolSwapResult + + //swap payload + payload := map[string]string{ + "amount": amount, + "from": from, + "fromTokenAddress": fromTokenAddress, + "poolAddress": poolAddress, + "toTokenAddress": toTokenAddress, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", config.PoolSwapURL, bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + return &r, nil +} + +func (as *HTTPAccountService) GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) { + if as.UseApi { + return as.getSwapFromTokenMaxLimit(ctx, poolAddress, fromTokenAddress, toTokenAddress, publicKey) + } else { + svc := dev.NewDevAccountService(ctx, as.SS) + return svc.GetSwapFromTokenMaxLimit(ctx, poolAddress, fromTokenAddress, toTokenAddress, publicKey) + } +} + +func (as *HTTPAccountService) getSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokeAddress, publicKey string) (*models.MaxLimitResult, error) { + var r models.MaxLimitResult + + ep, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "limit", fromTokenAddress, toTokeAddress, publicKey) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +func (as *HTTPAccountService) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) { + if as.UseApi { + return as.checkTokenInPool(ctx, poolAddress, tokenAddress) + } else { + svc := dev.NewDevAccountService(ctx, as.SS) + return svc.CheckTokenInPool(ctx, poolAddress, tokenAddress) + } +} + +func (as *HTTPAccountService) checkTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) { + var r models.TokenInPoolResult + + ep, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "check", tokenAddress) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +// TODO: Use actual custodial api to request available alias func (as *HTTPAccountService) RequestAlias(ctx context.Context, publicKey string, hint string) (*models.RequestAliasResult, error) { if as.SS == nil { return nil, fmt.Errorf("The storage service cannot be nil") @@ -285,11 +533,9 @@ func (as *HTTPAccountService) RequestAlias(ctx context.Context, publicKey string func requestEnsAlias(ctx context.Context, publicKey string, hint string) (*models.AliasEnsResult, error) { var r models.AliasEnsResult - ep, err := url.JoinPath(config.AliasEnsURL, "/register") - if err != nil { - return nil, err - } - logg.InfoCtxf(ctx, "requesting alias", "endpoint", ep, "hint", hint) + endpoint := config.AliasRegistrationURL + + logg.InfoCtxf(ctx, "requesting alias", "endpoint", endpoint, "hint", hint) //Payload with the address and hint to derive an ENS name payload := map[string]string{ "address": publicKey, @@ -299,7 +545,7 @@ func requestEnsAlias(ctx context.Context, publicKey string, hint string) (*model if err != nil { return nil, err } - req, err := http.NewRequest("POST", ep, bytes.NewBuffer(payloadBytes)) + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(payloadBytes)) if err != nil { return nil, err } @@ -402,6 +648,7 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons req.Header.Set("Authorization", "Bearer "+config.BearerToken) req.Header.Set("Content-Type", "application/json") + // Log request logRequestDetails(req) resp, err := http.DefaultClient.Do(req) @@ -412,22 +659,26 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons } defer resp.Body.Close() - log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type")) + // Read and log response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } + + log.Printf("Received response for %s: Status Code: %d | Content-Type: %s | Body: %s", + req.URL, resp.StatusCode, resp.Header.Get("Content-Type"), string(body)) + if resp.StatusCode >= http.StatusBadRequest { - err := json.Unmarshal([]byte(body), &errResponse) - if err != nil { + if err := json.Unmarshal(body, &errResponse); err != nil { return nil, err } return nil, errors.New(errResponse.Description) } - err = json.Unmarshal([]byte(body), &okResponse) - if err != nil { + + if err := json.Unmarshal(body, &okResponse); err != nil { return nil, err } + if len(okResponse.Result) == 0 { return nil, errors.New("Empty api result") } @@ -444,16 +695,14 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons func logRequestDetails(req *http.Request) { var bodyBytes []byte contentType := req.Header.Get("Content-Type") + if req.Body != nil { - bodyBytes, err := io.ReadAll(req.Body) - if err != nil { - log.Printf("Error reading request body: %s", err) - return - } - req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + bodyBytes, _ = io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Restore body } else { bodyBytes = []byte("-") } - log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes)) + log.Printf("Outgoing Request -> URL: %s | Method: %s | Content-Type: %s | Body: %s", + req.URL.String(), req.Method, contentType, string(bodyBytes)) } diff --git a/testutil/mocks/service_mock.go b/testutil/mocks/service_mock.go index e6bdd5e..93d0358 100644 --- a/testutil/mocks/service_mock.go +++ b/testutil/mocks/service_mock.go @@ -70,3 +70,48 @@ func (m *MockAccountService) SendPINResetSMS(ctx context.Context, admin, phone s func (m *MockAccountService) SendAddressSMS(ctx context.Context, publicKey, originPhone string) error { return nil } + +func (m MockAccountService) PoolDeposit(ctx context.Context, amount, from, poolAddress, tokenAddress string) (*models.PoolDepositResult, error) { + args := m.Called(amount, from, poolAddress, tokenAddress) + return args.Get(0).(*models.PoolDepositResult), args.Error(1) +} + +func (m MockAccountService) FetchTopPools(ctx context.Context) ([]dataserviceapi.PoolDetails, error) { + args := m.Called() + return args.Get(0).([]dataserviceapi.PoolDetails), args.Error(1) +} + +func (m MockAccountService) RetrievePoolDetails(ctx context.Context, sym string) (*dataserviceapi.PoolDetails, error) { + args := m.Called() + return args.Get(0).(*dataserviceapi.PoolDetails), args.Error(1) +} + +func (m MockAccountService) GetPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + args := m.Called(poolAddress, publicKey) + return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) +} + +func (m MockAccountService) GetPoolSwappableVouchers(ctx context.Context, poolAddress string) ([]dataserviceapi.TokenHoldings, error) { + args := m.Called(poolAddress) + return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) +} + +func (m MockAccountService) GetPoolSwapQuote(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapQuoteResult, error) { + args := m.Called(amount, from, fromTokenAddress, poolAddress, toTokenAddress) + return args.Get(0).(*models.PoolSwapQuoteResult), args.Error(1) +} + +func (m MockAccountService) PoolSwap(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapResult, error) { + args := m.Called(amount, from, fromTokenAddress, poolAddress, toTokenAddress) + return args.Get(0).(*models.PoolSwapResult), args.Error(1) +} + +func (m MockAccountService) GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) { + args := m.Called(poolAddress, fromTokenAddress, toTokenAddress, publicKey) + return args.Get(0).(*models.MaxLimitResult), args.Error(1) +} + +func (m MockAccountService) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) { + args := m.Called(poolAddress, tokenAddress) + return args.Get(0).(*models.TokenInPoolResult), args.Error(1) +} diff --git a/testutil/testservice/account_service.go b/testutil/testservice/account_service.go index abb7c92..935df73 100644 --- a/testutil/testservice/account_service.go +++ b/testutil/testservice/account_service.go @@ -35,10 +35,10 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { return []dataserviceapi.TokenHoldings{ dataserviceapi.TokenHoldings{ - ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", - TokenSymbol: "SRF", - TokenDecimals: "6", - Balance: "2745987", + TokenAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", + TokenSymbol: "SRF", + TokenDecimals: "6", + Balance: "2745987", }, }, nil } @@ -57,6 +57,10 @@ func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, }, nil } +func (m TestAccountService) PoolDeposit(ctx context.Context, amount, from, poolAddress, tokenAddress string) (*models.PoolDepositResult, error) { + return &models.PoolDepositResult{}, nil +} + func (m *TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*models.AliasAddress, error) { return &models.AliasAddress{}, nil } @@ -76,3 +80,35 @@ func (m *TestAccountService) SendAddressSMS(ctx context.Context, publicKey, orig func (m *TestAccountService) SendPINResetSMS(ctx context.Context, admin, phone string) error { return nil } + +func (m TestAccountService) FetchTopPools(ctx context.Context) ([]dataserviceapi.PoolDetails, error) { + return []dataserviceapi.PoolDetails{}, nil +} + +func (m TestAccountService) RetrievePoolDetails(ctx context.Context, sym string) (*dataserviceapi.PoolDetails, error) { + return &dataserviceapi.PoolDetails{}, nil +} + +func (m TestAccountService) GetPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + return []dataserviceapi.TokenHoldings{}, nil +} + +func (m TestAccountService) GetPoolSwappableVouchers(ctx context.Context, poolAddress string) ([]dataserviceapi.TokenHoldings, error) { + return []dataserviceapi.TokenHoldings{}, nil +} + +func (m TestAccountService) GetPoolSwapQuote(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapQuoteResult, error) { + return &models.PoolSwapQuoteResult{}, nil +} + +func (m TestAccountService) PoolSwap(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapResult, error) { + return &models.PoolSwapResult{}, nil +} + +func (m TestAccountService) GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) { + return &models.MaxLimitResult{}, nil +} + +func (m TestAccountService) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) { + return &models.TokenInPoolResult{}, nil +}