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  }