github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/internal/transform/contract_data.go (about) 1 package transform 2 3 import ( 4 "fmt" 5 "math/big" 6 7 "github.com/stellar/go/ingest" 8 "github.com/stellar/go/strkey" 9 "github.com/stellar/go/xdr" 10 "github.com/stellar/stellar-etl/internal/utils" 11 ) 12 13 const ( 14 scDecimalPrecision = 7 15 ) 16 17 var ( 18 // https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/public_types.rs#L22 19 nativeAssetSym = xdr.ScSymbol("Native") 20 // these are storage DataKey enum 21 // https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/storage_types.rs#L23 22 balanceMetadataSym = xdr.ScSymbol("Balance") 23 metadataSym = xdr.ScSymbol("METADATA") 24 metadataNameSym = xdr.ScSymbol("name") 25 metadataSymbolSym = xdr.ScSymbol("symbol") 26 adminSym = xdr.ScSymbol("Admin") 27 issuerSym = xdr.ScSymbol("issuer") 28 assetCodeSym = xdr.ScSymbol("asset_code") 29 alphaNum4Sym = xdr.ScSymbol("AlphaNum4") 30 alphaNum12Sym = xdr.ScSymbol("AlphaNum12") 31 decimalSym = xdr.ScSymbol("decimal") 32 assetInfoSym = xdr.ScSymbol("AssetInfo") 33 decimalVal = xdr.Uint32(scDecimalPrecision) 34 assetInfoVec = &xdr.ScVec{ 35 xdr.ScVal{ 36 Type: xdr.ScValTypeScvSymbol, 37 Sym: &assetInfoSym, 38 }, 39 } 40 assetInfoKey = xdr.ScVal{ 41 Type: xdr.ScValTypeScvVec, 42 Vec: &assetInfoVec, 43 } 44 ) 45 46 type AssetFromContractDataFunc func(ledgerEntry xdr.LedgerEntry, passphrase string) *xdr.Asset 47 type ContractBalanceFromContractDataFunc func(ledgerEntry xdr.LedgerEntry, passphrase string) ([32]byte, *big.Int, bool) 48 49 type TransformContractDataStruct struct { 50 AssetFromContractData AssetFromContractDataFunc 51 ContractBalanceFromContractData ContractBalanceFromContractDataFunc 52 } 53 54 func NewTransformContractDataStruct(assetFrom AssetFromContractDataFunc, contractBalance ContractBalanceFromContractDataFunc) *TransformContractDataStruct { 55 return &TransformContractDataStruct{ 56 AssetFromContractData: assetFrom, 57 ContractBalanceFromContractData: contractBalance, 58 } 59 } 60 61 // TransformContractData converts a contract data ledger change entry into a form suitable for BigQuery 62 func (t *TransformContractDataStruct) TransformContractData(ledgerChange ingest.Change, passphrase string, header xdr.LedgerHeaderHistoryEntry) (ContractDataOutput, error, bool) { 63 ledgerEntry, changeType, outputDeleted, err := utils.ExtractEntryFromChange(ledgerChange) 64 if err != nil { 65 return ContractDataOutput{}, err, false 66 } 67 68 contractData, ok := ledgerEntry.Data.GetContractData() 69 if !ok { 70 return ContractDataOutput{}, fmt.Errorf("Could not extract contract data from ledger entry; actual type is %s", ledgerEntry.Data.Type), false 71 } 72 73 if contractData.Key.Type.String() == "ScValTypeScvLedgerKeyNonce" { 74 // Is a nonce and should be discarded 75 return ContractDataOutput{}, nil, false 76 } 77 78 ledgerKeyHash := utils.LedgerEntryToLedgerKeyHash(ledgerEntry) 79 80 var contractDataAssetType string 81 var contractDataAssetCode string 82 var contractDataAssetIssuer string 83 84 contractDataAsset := t.AssetFromContractData(ledgerEntry, passphrase) 85 if contractDataAsset != nil { 86 contractDataAssetType = contractDataAsset.Type.String() 87 contractDataAssetCode = contractDataAsset.GetCode() 88 contractDataAssetIssuer = contractDataAsset.GetIssuer() 89 } 90 91 var contractDataBalanceHolder string 92 var contractDataBalance string 93 94 dataBalanceHolder, dataBalance, _ := t.ContractBalanceFromContractData(ledgerEntry, passphrase) 95 if dataBalance != nil { 96 holderHashByte, _ := xdr.Hash(dataBalanceHolder).MarshalBinary() 97 contractDataBalanceHolder, _ = strkey.Encode(strkey.VersionByteContract, holderHashByte) 98 contractDataBalance = dataBalance.String() 99 } 100 101 contractDataContractId, ok := contractData.Contract.GetContractId() 102 if !ok { 103 return ContractDataOutput{}, fmt.Errorf("Could not extract contractId data information from contractData"), false 104 } 105 106 contractDataKeyType := contractData.Key.Type.String() 107 contractDataContractIdByte, _ := contractDataContractId.MarshalBinary() 108 outputContractDataContractId, _ := strkey.Encode(strkey.VersionByteContract, contractDataContractIdByte) 109 110 contractDataDurability := contractData.Durability.String() 111 112 closedAt, err := utils.TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) 113 if err != nil { 114 return ContractDataOutput{}, err, false 115 } 116 117 ledgerSequence := header.Header.LedgerSeq 118 119 transformedData := ContractDataOutput{ 120 ContractId: outputContractDataContractId, 121 ContractKeyType: contractDataKeyType, 122 ContractDurability: contractDataDurability, 123 ContractDataAssetCode: contractDataAssetCode, 124 ContractDataAssetIssuer: contractDataAssetIssuer, 125 ContractDataAssetType: contractDataAssetType, 126 ContractDataBalanceHolder: contractDataBalanceHolder, 127 ContractDataBalance: contractDataBalance, 128 LastModifiedLedger: uint32(ledgerEntry.LastModifiedLedgerSeq), 129 LedgerEntryChange: uint32(changeType), 130 Deleted: outputDeleted, 131 ClosedAt: closedAt, 132 LedgerSequence: uint32(ledgerSequence), 133 LedgerKeyHash: ledgerKeyHash, 134 } 135 return transformedData, nil, true 136 } 137 138 // AssetFromContractData takes a ledger entry and verifies if the ledger entry 139 // corresponds to the asset info entry written to contract storage by the Stellar 140 // Asset Contract upon initialization. 141 // 142 // Note that AssetFromContractData will ignore forged asset info entries by 143 // deriving the Stellar Asset Contract ID from the asset info entry and comparing 144 // it to the contract ID found in the ledger entry. 145 // 146 // If the given ledger entry is a verified asset info entry, 147 // AssetFromContractData will return the corresponding Stellar asset. Otherwise, 148 // it returns nil. 149 // 150 // References: 151 // https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/public_types.rs#L21 152 // https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/asset_info.rs#L6 153 // https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/contract.rs#L115 154 // 155 // The asset info in `ContractData` entry takes the following form: 156 // 157 // - Instance storage - it's part of contract instance data storage 158 // 159 // - Key: a vector with one element, which is the symbol "AssetInfo" 160 // 161 // ScVal{ Vec: ScVec({ ScVal{ Sym: ScSymbol("AssetInfo") }})} 162 // 163 // - Value: a map with two key-value pairs: code and issuer 164 // 165 // ScVal{ Map: ScMap( 166 // { ScVal{ Sym: ScSymbol("asset_code") } -> ScVal{ Str: ScString(...) } }, 167 // { ScVal{ Sym: ScSymbol("issuer") } -> ScVal{ Bytes: ScBytes(...) } } 168 // )} 169 func AssetFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) *xdr.Asset { 170 contractData, ok := ledgerEntry.Data.GetContractData() 171 if !ok { 172 return nil 173 } 174 if contractData.Key.Type != xdr.ScValTypeScvLedgerKeyContractInstance { 175 return nil 176 } 177 contractInstanceData, ok := contractData.Val.GetInstance() 178 if !ok || contractInstanceData.Storage == nil { 179 return nil 180 } 181 182 nativeAssetContractID, err := xdr.MustNewNativeAsset().ContractID(passphrase) 183 if err != nil { 184 return nil 185 } 186 187 var assetInfo *xdr.ScVal 188 for _, mapEntry := range *contractInstanceData.Storage { 189 if mapEntry.Key.Equals(assetInfoKey) { 190 // clone the map entry to avoid reference to loop iterator 191 mapValXdr, cloneErr := mapEntry.Val.MarshalBinary() 192 if cloneErr != nil { 193 return nil 194 } 195 assetInfo = &xdr.ScVal{} 196 cloneErr = assetInfo.UnmarshalBinary(mapValXdr) 197 if cloneErr != nil { 198 return nil 199 } 200 break 201 } 202 } 203 204 if assetInfo == nil { 205 return nil 206 } 207 208 vecPtr, ok := assetInfo.GetVec() 209 if !ok || vecPtr == nil || len(*vecPtr) != 2 { 210 return nil 211 } 212 vec := *vecPtr 213 214 sym, ok := vec[0].GetSym() 215 if !ok { 216 return nil 217 } 218 switch sym { 219 case "AlphaNum4": 220 case "AlphaNum12": 221 case "Native": 222 if contractData.Contract.ContractId != nil && (*contractData.Contract.ContractId) == nativeAssetContractID { 223 asset := xdr.MustNewNativeAsset() 224 return &asset 225 } 226 default: 227 return nil 228 } 229 230 var assetCode, assetIssuer string 231 assetMapPtr, ok := vec[1].GetMap() 232 if !ok || assetMapPtr == nil || len(*assetMapPtr) != 2 { 233 return nil 234 } 235 assetMap := *assetMapPtr 236 237 assetCodeEntry, assetIssuerEntry := assetMap[0], assetMap[1] 238 if sym, ok = assetCodeEntry.Key.GetSym(); !ok || sym != assetCodeSym { 239 return nil 240 } 241 assetCodeSc, ok := assetCodeEntry.Val.GetStr() 242 if !ok { 243 return nil 244 } 245 if assetCode = string(assetCodeSc); assetCode == "" { 246 return nil 247 } 248 249 if sym, ok = assetIssuerEntry.Key.GetSym(); !ok || sym != issuerSym { 250 return nil 251 } 252 assetIssuerSc, ok := assetIssuerEntry.Val.GetBytes() 253 if !ok { 254 return nil 255 } 256 assetIssuer, err = strkey.Encode(strkey.VersionByteAccountID, assetIssuerSc) 257 if err != nil { 258 return nil 259 } 260 261 asset, err := xdr.NewCreditAsset(assetCode, assetIssuer) 262 if err != nil { 263 return nil 264 } 265 266 expectedID, err := asset.ContractID(passphrase) 267 if err != nil { 268 return nil 269 } 270 if contractData.Contract.ContractId == nil || expectedID != *(contractData.Contract.ContractId) { 271 return nil 272 } 273 274 return &asset 275 } 276 277 // ContractBalanceFromContractData takes a ledger entry and verifies that the 278 // ledger entry corresponds to the balance entry written to contract storage by 279 // the Stellar Asset Contract. 280 // 281 // Reference: 282 // 283 // https://github.com/stellar/rs-soroban-env/blob/da325551829d31dcbfa71427d51c18e71a121c5f/soroban-env-host/src/native_contract/token/storage_types.rs#L11-L24 284 func ContractBalanceFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) ([32]byte, *big.Int, bool) { 285 contractData, ok := ledgerEntry.Data.GetContractData() 286 if !ok { 287 return [32]byte{}, nil, false 288 } 289 290 _, err := xdr.MustNewNativeAsset().ContractID(passphrase) 291 if err != nil { 292 return [32]byte{}, nil, false 293 } 294 295 if contractData.Contract.ContractId == nil { 296 return [32]byte{}, nil, false 297 } 298 299 keyEnumVecPtr, ok := contractData.Key.GetVec() 300 if !ok || keyEnumVecPtr == nil { 301 return [32]byte{}, nil, false 302 } 303 keyEnumVec := *keyEnumVecPtr 304 if len(keyEnumVec) != 2 || !keyEnumVec[0].Equals( 305 xdr.ScVal{ 306 Type: xdr.ScValTypeScvSymbol, 307 Sym: &balanceMetadataSym, 308 }, 309 ) { 310 return [32]byte{}, nil, false 311 } 312 313 scAddress, ok := keyEnumVec[1].GetAddress() 314 if !ok { 315 return [32]byte{}, nil, false 316 } 317 318 holder, ok := scAddress.GetContractId() 319 if !ok { 320 return [32]byte{}, nil, false 321 } 322 323 balanceMapPtr, ok := contractData.Val.GetMap() 324 if !ok || balanceMapPtr == nil { 325 return [32]byte{}, nil, false 326 } 327 balanceMap := *balanceMapPtr 328 if !ok || len(balanceMap) != 3 { 329 return [32]byte{}, nil, false 330 } 331 332 var keySym xdr.ScSymbol 333 if keySym, ok = balanceMap[0].Key.GetSym(); !ok || keySym != "amount" { 334 return [32]byte{}, nil, false 335 } 336 if keySym, ok = balanceMap[1].Key.GetSym(); !ok || keySym != "authorized" || 337 !balanceMap[1].Val.IsBool() { 338 return [32]byte{}, nil, false 339 } 340 if keySym, ok = balanceMap[2].Key.GetSym(); !ok || keySym != "clawback" || 341 !balanceMap[2].Val.IsBool() { 342 return [32]byte{}, nil, false 343 } 344 amount, ok := balanceMap[0].Val.GetI128() 345 if !ok { 346 return [32]byte{}, nil, false 347 } 348 349 // amount cannot be negative 350 // https://github.com/stellar/rs-soroban-env/blob/a66f0815ba06a2f5328ac420950690fd1642f887/soroban-env-host/src/native_contract/token/balance.rs#L92-L93 351 if int64(amount.Hi) < 0 { 352 return [32]byte{}, nil, false 353 } 354 amt := new(big.Int).Lsh(new(big.Int).SetInt64(int64(amount.Hi)), 64) 355 amt.Add(amt, new(big.Int).SetUint64(uint64(amount.Lo))) 356 return holder, amt, true 357 }