github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/internal/transform/trade.go (about) 1 package transform 2 3 import ( 4 "fmt" 5 "math" 6 "time" 7 8 "github.com/guregu/null" 9 "github.com/pkg/errors" 10 11 "github.com/stellar/go/exp/orderbook" 12 "github.com/stellar/go/ingest" 13 "github.com/stellar/go/support/log" 14 "github.com/stellar/go/xdr" 15 "github.com/stellar/stellar-etl/internal/toid" 16 "github.com/stellar/stellar-etl/internal/utils" 17 ) 18 19 // TransformTrade converts a relevant operation from the history archive ingestion system into a form suitable for BigQuery 20 func TransformTrade(operationIndex int32, operationID int64, transaction ingest.LedgerTransaction, ledgerCloseTime time.Time) ([]TradeOutput, error) { 21 operationResults, ok := transaction.Result.OperationResults() 22 if !ok { 23 return []TradeOutput{}, fmt.Errorf("Could not get any results from this transaction") 24 } 25 26 if !transaction.Result.Successful() { 27 return []TradeOutput{}, fmt.Errorf("Transaction failed; no trades") 28 } 29 30 operation := transaction.Envelope.Operations()[operationIndex] 31 // operation id is +1 incremented to stay in sync with ingest package 32 outputOperationID := operationID + 1 33 claimedOffers, BuyingOffer, sellerIsExact, err := extractClaimedOffers(operationResults, operationIndex, operation.Body.Type) 34 if err != nil { 35 return []TradeOutput{}, err 36 } 37 38 transformedTrades := []TradeOutput{} 39 40 for claimOrder, claimOffer := range claimedOffers { 41 outputOrder := int32(claimOrder) 42 outputLedgerClosedAt := ledgerCloseTime 43 44 var outputSellingAssetType, outputSellingAssetCode, outputSellingAssetIssuer string 45 err = claimOffer.AssetSold().Extract(&outputSellingAssetType, &outputSellingAssetCode, &outputSellingAssetIssuer) 46 if err != nil { 47 return []TradeOutput{}, err 48 } 49 outputSellingAssetID := FarmHashAsset(outputSellingAssetCode, outputSellingAssetIssuer, outputSellingAssetType) 50 51 outputSellingAmount := claimOffer.AmountSold() 52 if outputSellingAmount < 0 { 53 return []TradeOutput{}, fmt.Errorf("Amount sold is negative (%d) for operation at index %d", outputSellingAmount, operationIndex) 54 } 55 56 var outputBuyingAssetType, outputBuyingAssetCode, outputBuyingAssetIssuer string 57 err = claimOffer.AssetBought().Extract(&outputBuyingAssetType, &outputBuyingAssetCode, &outputBuyingAssetIssuer) 58 if err != nil { 59 return []TradeOutput{}, err 60 } 61 outputBuyingAssetID := FarmHashAsset(outputBuyingAssetCode, outputBuyingAssetIssuer, outputBuyingAssetType) 62 63 outputBuyingAmount := int64(claimOffer.AmountBought()) 64 if outputBuyingAmount < 0 { 65 return []TradeOutput{}, fmt.Errorf("Amount bought is negative (%d) for operation at index %d", outputBuyingAmount, operationIndex) 66 } 67 68 if outputSellingAmount == 0 && outputBuyingAmount == 0 { 69 log.Debugf("Both Selling and Buying amount are 0 for operation at index %d", operationIndex) 70 continue 71 } 72 73 // Final price should be buy / sell 74 outputPriceN, outputPriceD, err := findTradeSellPrice(transaction, operationIndex, claimOffer) 75 if err != nil { 76 return []TradeOutput{}, err 77 } 78 79 var outputSellingAccountAddress string 80 var liquidityPoolID null.String 81 var outputPoolFee, roundingSlippageBips null.Int 82 var outputSellingOfferID, outputBuyingOfferID null.Int 83 var tradeType int32 84 if claimOffer.Type == xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool { 85 id := claimOffer.MustLiquidityPool().LiquidityPoolId 86 liquidityPoolID = null.StringFrom(PoolIDToString(id)) 87 tradeType = int32(2) 88 var fee uint32 89 if fee, err = findPoolFee(transaction, operationIndex, id); err != nil { 90 return []TradeOutput{}, fmt.Errorf("Cannot parse fee for liquidity pool %v", liquidityPoolID) 91 } 92 outputPoolFee = null.IntFrom(int64(fee)) 93 94 change, err := liquidityPoolChange(transaction, operationIndex, claimOffer) 95 if err != nil { 96 return nil, err 97 } 98 if change != nil { 99 roundingSlippageBips, err = roundingSlippage(transaction, operationIndex, claimOffer, change) 100 if err != nil { 101 return nil, err 102 } 103 } 104 } else { 105 outputSellingOfferID = null.IntFrom(int64(claimOffer.OfferId())) 106 outputSellingAccountAddress = claimOffer.SellerId().Address() 107 tradeType = int32(1) 108 } 109 110 if BuyingOffer != nil { 111 outputBuyingOfferID = null.IntFrom(int64(BuyingOffer.OfferId)) 112 } else { 113 outputBuyingOfferID = null.IntFrom(toid.EncodeOfferId(uint64(operationID)+1, toid.TOIDType)) 114 } 115 116 var outputBuyingAccountAddress string 117 if buyer := operation.SourceAccount; buyer != nil { 118 accid := buyer.ToAccountId() 119 outputBuyingAccountAddress = accid.Address() 120 } else { 121 sa := transaction.Envelope.SourceAccount().ToAccountId() 122 outputBuyingAccountAddress = sa.Address() 123 } 124 125 trade := TradeOutput{ 126 Order: outputOrder, 127 LedgerClosedAt: outputLedgerClosedAt, 128 SellingAccountAddress: outputSellingAccountAddress, 129 SellingAssetType: outputSellingAssetType, 130 SellingAssetCode: outputSellingAssetCode, 131 SellingAssetIssuer: outputSellingAssetIssuer, 132 SellingAssetID: outputSellingAssetID, 133 SellingAmount: utils.ConvertStroopValueToReal(outputSellingAmount), 134 BuyingAccountAddress: outputBuyingAccountAddress, 135 BuyingAssetType: outputBuyingAssetType, 136 BuyingAssetCode: outputBuyingAssetCode, 137 BuyingAssetIssuer: outputBuyingAssetIssuer, 138 BuyingAssetID: outputBuyingAssetID, 139 BuyingAmount: utils.ConvertStroopValueToReal(xdr.Int64(outputBuyingAmount)), 140 PriceN: outputPriceN, 141 PriceD: outputPriceD, 142 SellingOfferID: outputSellingOfferID, 143 BuyingOfferID: outputBuyingOfferID, 144 SellingLiquidityPoolID: liquidityPoolID, 145 LiquidityPoolFee: outputPoolFee, 146 HistoryOperationID: outputOperationID, 147 TradeType: tradeType, 148 RoundingSlippage: roundingSlippageBips, 149 SellerIsExact: sellerIsExact, 150 } 151 152 transformedTrades = append(transformedTrades, trade) 153 } 154 return transformedTrades, nil 155 } 156 157 func extractClaimedOffers(operationResults []xdr.OperationResult, operationIndex int32, operationType xdr.OperationType) (claimedOffers []xdr.ClaimAtom, BuyingOffer *xdr.OfferEntry, sellerIsExact null.Bool, err error) { 158 if operationIndex >= int32(len(operationResults)) { 159 err = fmt.Errorf("Operation index of %d is out of bounds in result slice (len = %d)", operationIndex, len(operationResults)) 160 return 161 } 162 163 if operationResults[operationIndex].Tr == nil { 164 err = fmt.Errorf("Could not get result Tr for operation at index %d", operationIndex) 165 return 166 } 167 168 operationTr, ok := operationResults[operationIndex].GetTr() 169 if !ok { 170 err = fmt.Errorf("Could not get result Tr for operation at index %d", operationIndex) 171 return 172 } 173 switch operationType { 174 case xdr.OperationTypeManageBuyOffer: 175 var buyOfferResult xdr.ManageBuyOfferResult 176 if buyOfferResult, ok = operationTr.GetManageBuyOfferResult(); !ok { 177 err = fmt.Errorf("Could not get ManageBuyOfferResult for operation at index %d", operationIndex) 178 return 179 } 180 if success, ok := buyOfferResult.GetSuccess(); ok { 181 claimedOffers = success.OffersClaimed 182 BuyingOffer = success.Offer.Offer 183 return 184 } 185 186 err = fmt.Errorf("Could not get ManageOfferSuccess for operation at index %d", operationIndex) 187 188 case xdr.OperationTypeManageSellOffer: 189 var sellOfferResult xdr.ManageSellOfferResult 190 if sellOfferResult, ok = operationTr.GetManageSellOfferResult(); !ok { 191 err = fmt.Errorf("Could not get ManageSellOfferResult for operation at index %d", operationIndex) 192 return 193 } 194 195 if success, ok := sellOfferResult.GetSuccess(); ok { 196 claimedOffers = success.OffersClaimed 197 BuyingOffer = success.Offer.Offer 198 return 199 } 200 201 err = fmt.Errorf("Could not get ManageOfferSuccess for operation at index %d", operationIndex) 202 203 case xdr.OperationTypeCreatePassiveSellOffer: 204 // KNOWN ISSUE: stellar-core creates results for CreatePassiveOffer operations 205 // with the wrong result arm set. 206 if operationTr.Type == xdr.OperationTypeManageSellOffer { 207 passiveSellResult := operationTr.MustManageSellOfferResult().MustSuccess() 208 claimedOffers = passiveSellResult.OffersClaimed 209 BuyingOffer = passiveSellResult.Offer.Offer 210 return 211 } else { 212 passiveSellResult := operationTr.MustCreatePassiveSellOfferResult().MustSuccess() 213 claimedOffers = passiveSellResult.OffersClaimed 214 BuyingOffer = passiveSellResult.Offer.Offer 215 return 216 } 217 218 case xdr.OperationTypePathPaymentStrictSend: 219 var pathSendResult xdr.PathPaymentStrictSendResult 220 sellerIsExact = null.BoolFrom(false) 221 if pathSendResult, ok = operationTr.GetPathPaymentStrictSendResult(); !ok { 222 err = fmt.Errorf("Could not get PathPaymentStrictSendResult for operation at index %d", operationIndex) 223 return 224 } 225 226 success, ok := pathSendResult.GetSuccess() 227 if ok { 228 claimedOffers = success.Offers 229 return 230 } 231 232 err = fmt.Errorf("Could not get PathPaymentStrictSendSuccess for operation at index %d", operationIndex) 233 234 case xdr.OperationTypePathPaymentStrictReceive: 235 var pathReceiveResult xdr.PathPaymentStrictReceiveResult 236 sellerIsExact = null.BoolFrom(true) 237 if pathReceiveResult, ok = operationTr.GetPathPaymentStrictReceiveResult(); !ok { 238 err = fmt.Errorf("Could not get PathPaymentStrictReceiveResult for operation at index %d", operationIndex) 239 return 240 } 241 242 if success, ok := pathReceiveResult.GetSuccess(); ok { 243 claimedOffers = success.Offers 244 return 245 } 246 247 err = fmt.Errorf("Could not get GetPathPaymentStrictReceiveSuccess for operation at index %d", operationIndex) 248 249 default: 250 err = fmt.Errorf("Operation of type %s at index %d does not result in trades", operationType, operationIndex) 251 return 252 } 253 254 return 255 } 256 257 func findTradeSellPrice(t ingest.LedgerTransaction, operationIndex int32, trade xdr.ClaimAtom) (n, d int64, err error) { 258 if trade.Type == xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool { 259 return int64(trade.AmountBought()), int64(trade.AmountSold()), nil 260 } 261 262 key := xdr.LedgerKey{} 263 if err := key.SetOffer(trade.SellerId(), uint64(trade.OfferId())); err != nil { 264 return 0, 0, errors.Wrap(err, "Could not create offer ledger key") 265 } 266 change, err := findLatestOperationChange(t, operationIndex, key) 267 if err != nil { 268 return 0, 0, errors.Wrap(err, "could not find change for trade offer") 269 } 270 271 return int64(change.Pre.Data.MustOffer().Price.N), int64(change.Pre.Data.MustOffer().Price.D), nil 272 } 273 274 func findLatestOperationChange(t ingest.LedgerTransaction, operationIndex int32, key xdr.LedgerKey) (ingest.Change, error) { 275 changes, err := t.GetOperationChanges(uint32(operationIndex)) 276 if err != nil { 277 return ingest.Change{}, errors.Wrap(err, "could not determine changes for operation") 278 } 279 280 var change ingest.Change 281 // traverse through the slice in reverse order 282 for i := len(changes) - 1; i >= 0; i-- { 283 change = changes[i] 284 if change.Pre != nil { 285 preKey, err := change.Pre.LedgerKey() 286 if err != nil { 287 return ingest.Change{}, errors.Wrap(err, "could not determine ledger key for change") 288 289 } 290 if key.Equals(preKey) { 291 return change, nil 292 } 293 } 294 295 } 296 return ingest.Change{}, errors.Errorf("could not find operation for key %v", key) 297 } 298 299 func findPoolFee(t ingest.LedgerTransaction, operationIndex int32, poolID xdr.PoolId) (fee uint32, err error) { 300 key := xdr.LedgerKey{} 301 if err := key.SetLiquidityPool(poolID); err != nil { 302 return 0, errors.Wrap(err, "Could not create liquidity pool ledger key") 303 } 304 change, err := findLatestOperationChange(t, operationIndex, key) 305 if err != nil { 306 return 0, errors.Wrap(err, "could not find change for liquidity pool") 307 } 308 309 return uint32(change.Pre.Data.MustLiquidityPool().Body.MustConstantProduct().Params.Fee), nil 310 } 311 312 func liquidityPoolChange(t ingest.LedgerTransaction, operationIndex int32, trade xdr.ClaimAtom) (*ingest.Change, error) { 313 if trade.Type != xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool { 314 return nil, nil 315 } 316 317 poolID := trade.LiquidityPool.LiquidityPoolId 318 319 key := xdr.LedgerKey{} 320 if err := key.SetLiquidityPool(poolID); err != nil { 321 return nil, errors.Wrap(err, "Could not create liquidity pool ledger key") 322 } 323 324 change, err := findLatestOperationChange(t, operationIndex, key) 325 if err != nil { 326 return nil, errors.Wrap(err, "Could not find change for liquidity pool") 327 } 328 329 return &change, nil 330 } 331 332 func liquidityPoolReserves(trade xdr.ClaimAtom, change *ingest.Change) (int64, int64) { 333 pre := change.Pre.Data.MustLiquidityPool().Body.ConstantProduct 334 a := int64(pre.ReserveA) 335 b := int64(pre.ReserveB) 336 if !trade.AssetSold().Equals(pre.Params.AssetA) { 337 a, b = b, a 338 } 339 340 return a, b 341 } 342 343 func roundingSlippage(t ingest.LedgerTransaction, operationIndex int32, trade xdr.ClaimAtom, change *ingest.Change) (null.Int, error) { 344 disbursedReserves, depositedReserves := liquidityPoolReserves(trade, change) 345 346 pre := change.Pre.Data.MustLiquidityPool().Body.ConstantProduct 347 348 op, found := t.GetOperation(uint32(operationIndex)) 349 if !found { 350 return null.Int{}, errors.New("Could not find operation") 351 } 352 353 amountDeposited := trade.AmountBought() 354 amountDisbursed := trade.AmountSold() 355 356 switch op.Body.Type { 357 case xdr.OperationTypePathPaymentStrictReceive: 358 // User specified the disbursed amount 359 _, roundingSlippageBips, ok := orderbook.CalculatePoolPayout( 360 xdr.Int64(depositedReserves), 361 xdr.Int64(disbursedReserves), 362 amountDisbursed, 363 pre.Params.Fee, 364 true, 365 ) 366 if !ok { 367 // This is a temporary workaround and will be addressed when 368 // https://github.com/stellar/go/issues/4203 is closed 369 roundingSlippageBips = xdr.Int64(math.MaxInt64) 370 } 371 return null.IntFrom(int64(roundingSlippageBips)), nil 372 case xdr.OperationTypePathPaymentStrictSend: 373 // User specified the deposited amount 374 _, roundingSlippageBips, ok := orderbook.CalculatePoolPayout( 375 xdr.Int64(depositedReserves), 376 xdr.Int64(disbursedReserves), 377 amountDeposited, 378 pre.Params.Fee, 379 true, 380 ) 381 if !ok { 382 // Temporary workaround for https://github.com/stellar/go/issues/4203 383 // Given strict receives that would overflow here, minimum slippage 384 // so they get excluded. 385 roundingSlippageBips = xdr.Int64(math.MinInt64) 386 } 387 return null.IntFrom(int64(roundingSlippageBips)), nil 388 default: 389 return null.Int{}, fmt.Errorf("Unexpected trade operation type: %v", op.Body.Type) 390 } 391 392 }