github.com/DapperCollectives/CAST/backend@v0.0.0-20230921221157-1350c8be7c96/main/shared/flow.go (about) 1 package shared 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "flag" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "regexp" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/rs/zerolog/log" 17 18 "github.com/onflow/cadence" 19 "github.com/onflow/flow-go-sdk" 20 "github.com/onflow/flow-go-sdk/client" 21 "google.golang.org/grpc" 22 ) 23 24 type FlowAdapter struct { 25 Config FlowConfig 26 ArchiveClient *client.Client 27 LiveClient *client.Client 28 Context context.Context 29 CustomScriptsMap map[string]CustomScript 30 URL string 31 ArchiveURL string 32 Env string 33 } 34 35 type FlowContract struct { 36 Source string `json:"source,omitempty"` 37 Aliases map[string]string `json:"aliases"` 38 } 39 40 type FlowConfig struct { 41 Contracts map[string]FlowContract `json:"contracts"` 42 Networks map[string]string `json:"networks"` 43 } 44 45 type Contract struct { 46 Name *string `json:"name,omitempty"` 47 Addr *string `json:"addr,omitempty"` 48 Public_path *string `json:"publicPath,omitempty"` 49 Threshold *float64 `json:"threshold,omitempty,string"` 50 MaxWeight *float64 `json:"maxWeight,omitempty,string"` 51 Float_event_id *uint64 `json:"floatEventId,omitempty,string"` 52 Script *string `json:"script,omitempty"` 53 } 54 55 var ( 56 placeholderTokenName = regexp.MustCompile(`"[^"\s]*TOKEN_NAME"`) 57 placeholderTokenAddr = regexp.MustCompile(`"[^"\s]*TOKEN_ADDRESS"`) 58 placeholderFungibleTokenAddr = regexp.MustCompile(`"[^"\s]*FUNGIBLE_TOKEN_ADDRESS"`) 59 placeholderNonFungibleTokenAddr = regexp.MustCompile(`"[^"\s]*NON_FUNGIBLE_TOKEN_ADDRESS"`) 60 placeholderMetadataViewsAddr = regexp.MustCompile(`"[^"\s]*METADATA_VIEWS_ADDRESS"`) 61 placeholderCollectionPublicPath = regexp.MustCompile(`"[^"\s]*COLLECTION_PUBLIC_PATH"`) 62 placeholderTopshotAddr = regexp.MustCompile(`"[^"\s]*TOPSHOT_ADDRESS"`) 63 ) 64 65 func NewFlowClient(flowEnv string, customScriptsMap map[string]CustomScript) *FlowAdapter { 66 adapter := FlowAdapter{} 67 adapter.Context = context.Background() 68 adapter.Env = flowEnv 69 adapter.CustomScriptsMap = customScriptsMap 70 path := "./flow.json" 71 72 content, err := ioutil.ReadFile(path) 73 74 if err != nil { 75 log.Fatal().Msgf("Error when opening file: %+v.", err) 76 } 77 78 var config FlowConfig 79 err = json.Unmarshal(content, &config) 80 if err != nil { 81 log.Fatal().Msgf("Error parsing flow.json: %+v.", err) 82 } 83 84 adapter.Config = config 85 adapter.URL = config.Networks[adapter.Env] 86 adapter.ArchiveURL = config.Networks[fmt.Sprintf("%s_archive", adapter.Env)] 87 88 // Explicitly set when running test suite 89 if flag.Lookup("test.v") != nil { 90 adapter.URL = "127.0.0.1:3569" 91 adapter.ArchiveURL = "127.0.0.1:3569" 92 } 93 94 log.Info().Msgf("FLOW URL: %s", adapter.URL) 95 // create flow client 96 FlowClient, err := client.New(adapter.URL, grpc.WithInsecure()) 97 if err != nil { 98 log.Panic().Msgf("Failed to connect to %s.", adapter.URL) 99 } 100 101 log.Info().Msgf("FLOW Archive URL: %s", adapter.ArchiveURL) 102 103 //create archive client 104 FlowClientArchive, err := client.New(adapter.ArchiveURL, grpc.WithInsecure()) 105 if err != nil { 106 log.Panic().Msgf("Failed to connect to %s.", adapter.ArchiveURL) 107 } 108 109 adapter.LiveClient = FlowClient 110 adapter.ArchiveClient = FlowClientArchive 111 112 return &adapter 113 } 114 115 func (fa *FlowAdapter) GetAccountAtBlockHeight(addr string, blockheight uint64) (*flow.Account, error) { 116 hexAddr := flow.HexToAddress(addr) 117 return fa.ArchiveClient.GetAccountAtBlockHeight(fa.Context, hexAddr, blockheight) 118 } 119 120 func (fa *FlowAdapter) GetCurrentBlockHeight() (int, error) { 121 block, err := fa.LiveClient.GetLatestBlock(fa.Context, true) 122 if err != nil { 123 return 0, err 124 } 125 return int(block.Height), nil 126 } 127 128 func (fa *FlowAdapter) GetAddressBalanceAtBlockHeight(addr string, blockHeight uint64, balanceResponse *FTBalanceResponse, contract *Contract) error { 129 130 if *contract.Name == "FlowToken" { 131 balances, err := fa.GetFlowBalance(addr, blockHeight) 132 fmt.Println(balances) 133 if err != nil { 134 return err 135 } 136 balanceResponse.PrimaryAccountBalance = uint64(balances[0] * 100000000.0) 137 balanceResponse.SecondaryAccountBalance = uint64(balances[1] * 10000000.0) 138 balanceResponse.StakingBalance = uint64(balances[2] * 10000000.0) 139 140 return nil 141 142 } else { 143 balance, err := fa.GetFTBalance(addr, blockHeight, *contract.Name, *contract.Addr, *contract.Public_path) 144 fmt.Println(balance) 145 if err != nil { 146 return err 147 } 148 balanceResponse.PrimaryAccountBalance = uint64(balance * 100000000.0) 149 balanceResponse.SecondaryAccountBalance = 0 150 balanceResponse.StakingBalance = 0 151 return nil 152 } 153 } 154 155 func (fa *FlowAdapter) ValidateSignature(address, message string, sigs *[]CompositeSignature, messageType string) error { 156 log.Debug().Msgf("ValidateSignature()\nAddress: %s\nMessage: %s\nSigs: %v.", address, message, *sigs) 157 158 // Prepare Script Args 159 flowAddress := flow.HexToAddress(address) 160 cadenceAddress := cadence.NewAddress(flowAddress) 161 cadenceString, err := cadence.NewString(message) 162 163 // Pull out signature strings + keyIds for script 164 var cadenceSigs []cadence.Value = make([]cadence.Value, len(*sigs)) 165 var cadenceKeyIds []cadence.Value = make([]cadence.Value, len(*sigs)) 166 for i, cSig := range *sigs { 167 cadenceKeyIds[i] = cadence.NewInt(int(cSig.Key_id)) 168 cadenceSigs[i] = cadence.String(cSig.Signature) 169 } 170 171 var domainSeparationTag string 172 if messageType == "TRANSACTION" { 173 domainSeparationTag = "FLOW-V0.0-transaction" 174 } else { 175 domainSeparationTag = "FLOW-V0.0-user" 176 } 177 178 // Load script 179 script, err := ioutil.ReadFile("./main/cadence/scripts/validate_signature.cdc") 180 181 if err != nil { 182 log.Error().Err(err).Msgf("Error reading cadence script file.") 183 return err 184 } 185 186 // call the script to verify the signature on chain 187 value, err := fa.LiveClient.ExecuteScriptAtLatestBlock( 188 fa.Context, 189 script, 190 []cadence.Value{ 191 cadenceAddress, 192 cadence.NewArray(cadenceKeyIds), 193 cadence.NewArray(cadenceSigs), 194 cadence.String(cadenceString), 195 cadence.String(domainSeparationTag), 196 }, 197 ) 198 199 log.Info().Msgf("Validate signature script returned: %v", value) 200 201 if err != nil && strings.Contains(err.Error(), "ledger returns unsuccessful") { 202 log.Error().Err(err).Msg("signature validation error") 203 return errors.New("flow access node error, please cast your vote again") 204 } else if err != nil { 205 log.Error().Err(err).Msg("Signature validation error.") 206 return err 207 } 208 209 if err != nil { 210 return err 211 } 212 213 if value != cadence.NewBool(true) { 214 return errors.New("invalid signature") 215 } 216 217 return nil 218 219 } 220 221 func (fa *FlowAdapter) EnforceTokenThreshold(scriptPath, creatorAddr string, c *Contract) (bool, error) { 222 223 var balance float64 224 flowAddress := flow.HexToAddress(creatorAddr) 225 cadenceAddress := cadence.NewAddress(flowAddress) 226 cadencePath := cadence.Path{Domain: "public", Identifier: *c.Public_path} 227 fmt.Println(cadencePath) 228 script, err := ioutil.ReadFile(scriptPath) 229 if err != nil { 230 log.Error().Err(err).Msgf("Error reading cadence script file.") 231 return false, err 232 } 233 234 var cadenceValue cadence.Value 235 236 if scriptPath == "./main/cadence/scripts/get_nfts_ids.cdc" { 237 isFungible := false 238 script = fa.ReplaceContractPlaceholders(string(script[:]), c, isFungible) 239 240 //call the non-fungible token script to verify balance 241 cadenceValue, err = fa.LiveClient.ExecuteScriptAtLatestBlock( 242 fa.Context, 243 script, 244 []cadence.Value{ 245 cadenceAddress, 246 }) 247 if err != nil { 248 log.Error().Err(err).Msg("Error executing Non-Fungible-Token script.") 249 return false, err 250 } 251 value := CadenceValueToInterface(cadenceValue) 252 253 nftIds := value.([]interface{}) 254 balance = float64(len(nftIds)) 255 256 } else { 257 isFungible := true 258 script = fa.ReplaceContractPlaceholders(string(script[:]), c, isFungible) 259 260 //call the fungible-token script to verify balance 261 cadenceValue, err = fa.LiveClient.ExecuteScriptAtLatestBlock( 262 fa.Context, 263 script, 264 []cadence.Value{ 265 cadencePath, 266 cadenceAddress, 267 }) 268 if err != nil { 269 log.Error().Err(err).Msg("Error executing Fungible-Token Script.") 270 return false, err 271 } 272 273 value := CadenceValueToInterface(cadenceValue) 274 balance, err = strconv.ParseFloat(value.(string), 64) 275 if err != nil { 276 log.Error().Err(err).Msg("Error converting cadence value to float.") 277 return false, err 278 } 279 } 280 281 //check if balance is greater than threshold 282 if balance < *c.Threshold { 283 return false, nil 284 } 285 286 return true, nil 287 } 288 289 func (fa *FlowAdapter) GetFlowBalance(address string, blockHeight uint64) ([]float64, error) { 290 flowAddress := flow.HexToAddress(address) 291 cadenceAddress := cadence.NewAddress(flowAddress) 292 script, err := ioutil.ReadFile("./main/cadence/scripts/get_total_balance.cdc") 293 if err != nil { 294 log.Error().Err(err).Msgf("Error reading cadence script file.") 295 return []float64{0, 0, 0}, err 296 } 297 cadenceValue, err := fa.ArchiveClient.ExecuteScriptAtBlockHeight( 298 fa.Context, 299 blockHeight, 300 script, 301 []cadence.Value{ 302 cadenceAddress, 303 }, 304 ) 305 306 if err != nil { 307 log.Error().Err(err).Msg("Error executing Total balance Script.") 308 return []float64{0, 0, 0}, err 309 } 310 311 var values []interface{} = CadenceValueToInterface(cadenceValue).([]interface{}) 312 313 balancePrimary, err := strconv.ParseFloat(values[0].(string), 64) 314 if err != nil { 315 log.Error().Err(err).Msg("Error converting cadence value to float. (balancePrimary)") 316 return []float64{0, 0, 0}, err 317 } 318 319 balanceSecondary, err := strconv.ParseFloat(values[1].(string), 64) 320 if err != nil { 321 log.Error().Err(err).Msg("Error converting cadence value to float. (balanceSecondary)") 322 return []float64{0, 0, 0}, err 323 } 324 325 balanceStaked, err := strconv.ParseFloat(values[2].(string), 64) 326 if err != nil { 327 log.Error().Err(err).Msg("Error converting cadence value to float. (balanceStaked)") 328 return []float64{0, 0, 0}, err 329 } 330 return []float64{balancePrimary, balanceSecondary, balanceStaked}, nil 331 } 332 333 // @bluesign: this is called via archival node now 334 func (fa *FlowAdapter) GetFTBalance(address string, blockHeight uint64, contractName string, contractAddress string, publicPath string) (float64, error) { 335 flowAddress := flow.HexToAddress(address) 336 cadenceAddress := cadence.NewAddress(flowAddress) 337 338 script, err := ioutil.ReadFile("./main/cadence/scripts/get_balance.cdc") 339 if err != nil { 340 log.Error().Err(err).Msgf("Error reading cadence script file.") 341 return 0, err 342 } 343 344 dummyContract := Contract{ 345 Name: &contractName, 346 Public_path: &publicPath, 347 Addr: &contractAddress, 348 } 349 350 script = fa.ReplaceContractPlaceholders(string(script[:]), &dummyContract, true) 351 cadencePath := cadence.Path{Domain: "public", Identifier: *dummyContract.Public_path} 352 cadenceValue, err := fa.ArchiveClient.ExecuteScriptAtBlockHeight( 353 fa.Context, 354 blockHeight, 355 script, 356 []cadence.Value{ 357 cadencePath, 358 cadenceAddress, 359 }) 360 if err != nil { 361 log.Error().Err(err).Msg("Error executing Funigble-Token Script.") 362 return 0, err 363 } 364 365 value := CadenceValueToInterface(cadenceValue) 366 balance, err := strconv.ParseFloat(value.(string), 64) 367 if err != nil { 368 log.Error().Err(err).Msg("Error converting cadence value to float.") 369 return 0, err 370 } 371 372 return balance, nil 373 } 374 375 func (fa *FlowAdapter) GetNFTIds(voterAddr string, c *Contract, path string) ([]interface{}, error) { 376 flowAddress := flow.HexToAddress(voterAddr) 377 cadenceAddress := cadence.NewAddress(flowAddress) 378 379 script, err := ioutil.ReadFile(path) 380 if err != nil { 381 log.Error().Err(err).Msgf("Error reading cadence script file.") 382 return nil, err 383 } 384 385 script = fa.ReplaceContractPlaceholders(string(script[:]), c, false) 386 387 cadenceValue, err := fa.LiveClient.ExecuteScriptAtLatestBlock( 388 fa.Context, 389 script, 390 []cadence.Value{ 391 cadenceAddress, 392 }, 393 ) 394 if err != nil { 395 log.Error().Err(err).Msg("Error executing script.") 396 return nil, err 397 } 398 399 value := CadenceValueToInterface(cadenceValue) 400 401 nftIds := value.([]interface{}) 402 return nftIds, nil 403 } 404 405 func (fa *FlowAdapter) GetFloatNFTIds(voterAddr string, c *Contract) ([]interface{}, error) { 406 flowAddress := flow.HexToAddress(voterAddr) 407 cadenceAddress := cadence.NewAddress(flowAddress) 408 cadenceUInt64 := cadence.NewUInt64(*c.Float_event_id) 409 410 script, err := ioutil.ReadFile("./main/cadence/float/scripts/get_float_ids.cdc") 411 if err != nil { 412 log.Error().Err(err).Msgf("Error reading cadence script file.") 413 return nil, err 414 } 415 416 script = fa.ReplaceContractPlaceholders(string(script[:]), c, false) 417 418 cadenceValue, err := fa.LiveClient.ExecuteScriptAtLatestBlock( 419 fa.Context, 420 script, 421 []cadence.Value{ 422 cadenceAddress, 423 cadenceUInt64, 424 }) 425 if err != nil { 426 log.Error().Err(err).Msg("Error executing script.") 427 return nil, err 428 } 429 430 value := CadenceValueToInterface(cadenceValue) 431 432 nftIds := value.([]interface{}) 433 return nftIds, nil 434 } 435 436 func (fa *FlowAdapter) CheckIfUserHasEvent(voterAddr string, c *Contract) (bool, error) { 437 flowAddress := flow.HexToAddress(voterAddr) 438 cadenceAddress := cadence.NewAddress(flowAddress) 439 cadenceUInt64 := cadence.NewUInt64(*c.Float_event_id) 440 441 script, err := ioutil.ReadFile("./main/cadence/float/scripts/owns_specific_float.cdc") 442 if err != nil { 443 log.Error().Err(err).Msgf("Error reading cadence script file.") 444 return false, err 445 } 446 447 script = fa.ReplaceContractPlaceholders(string(script[:]), c, false) 448 449 cadenceValue, err := fa.LiveClient.ExecuteScriptAtLatestBlock( 450 fa.Context, 451 script, 452 []cadence.Value{ 453 cadenceAddress, 454 cadenceUInt64, 455 }) 456 if err != nil { 457 log.Error().Err(err).Msg("Error executing script.") 458 return false, err 459 } 460 461 value := CadenceValueToInterface(cadenceValue) 462 463 hasEventNFT := false 464 if value == "true" { 465 hasEventNFT = true 466 } 467 return hasEventNFT, nil 468 } 469 470 func (fa *FlowAdapter) GetEventNFT(voterAddr string, c *Contract) (interface{}, error) { 471 472 //first we get all the floats a user owns 473 474 flowAddress := flow.HexToAddress(voterAddr) 475 cadenceAddress := cadence.NewAddress(flowAddress) 476 cadenceUInt64 := cadence.NewUInt64(*c.Float_event_id) 477 478 script, err := ioutil.ReadFile("./main/cadence/float/scripts/get_specific_float.cdc") 479 if err != nil { 480 log.Error().Err(err).Msgf("Error reading cadence script file.") 481 return nil, err 482 } 483 484 script = fa.ReplaceContractPlaceholders(string(script[:]), c, false) 485 486 cadenceValue, err := fa.LiveClient.ExecuteScriptAtLatestBlock( 487 fa.Context, 488 script, 489 []cadence.Value{ 490 cadenceAddress, 491 cadenceUInt64, 492 }) 493 if err != nil { 494 log.Error().Err(err).Msg("Error executing script.") 495 return nil, err 496 } 497 498 value := CadenceValueToInterface(cadenceValue) 499 500 return value, nil 501 } 502 503 func (fa *FlowAdapter) ReplaceContractPlaceholders(code string, c *Contract, isFungible bool) []byte { 504 var ( 505 fungibleTokenAddr string 506 nonFungibleTokenAddr string 507 metadataViewsAddr string 508 topshotAddr string 509 ) 510 511 nonFungibleTokenAddr = fa.Config.Contracts["NonFungibleToken"].Aliases[os.Getenv("FLOW_ENV")] 512 fungibleTokenAddr = fa.Config.Contracts["FungibleToken"].Aliases[os.Getenv("FLOW_ENV")] 513 metadataViewsAddr = fa.Config.Contracts["MetadataViews"].Aliases[os.Getenv("FLOW_ENV")] 514 topshotAddr = fa.Config.Contracts["TopShot"].Aliases[os.Getenv("FLOW_ENV")] 515 516 if isFungible { 517 code = placeholderFungibleTokenAddr.ReplaceAllString(code, fungibleTokenAddr) 518 } else { 519 code = placeholderCollectionPublicPath.ReplaceAllString(code, *c.Public_path) 520 code = placeholderNonFungibleTokenAddr.ReplaceAllString(code, nonFungibleTokenAddr) 521 } 522 523 code = placeholderMetadataViewsAddr.ReplaceAllString(code, metadataViewsAddr) 524 code = placeholderTokenName.ReplaceAllString(code, *c.Name) 525 code = placeholderTokenAddr.ReplaceAllString(code, *c.Addr) 526 code = placeholderTopshotAddr.ReplaceAllString(code, topshotAddr) 527 528 return []byte(code) 529 } 530 531 func WaitForSeal( 532 ctx context.Context, 533 c *client.Client, 534 id flow.Identifier, 535 ) (*flow.TransactionResult, *flow.Transaction, error) { 536 result, err := c.GetTransactionResult(ctx, id) 537 if err != nil { 538 return nil, nil, err 539 } 540 541 for result.Status != flow.TransactionStatusSealed { 542 time.Sleep(time.Second) 543 result, err = c.GetTransactionResult(ctx, id) 544 if err != nil { 545 return nil, nil, err 546 } 547 } 548 549 tx, errTx := c.GetTransaction(ctx, id) 550 return result, tx, errTx 551 } 552 553 func CadenceValueToInterface(field cadence.Value) interface{} { 554 if field == nil { 555 return "" 556 } 557 558 switch field := field.(type) { 559 case cadence.Optional: 560 return CadenceValueToInterface(field.Value) 561 case cadence.Dictionary: 562 result := map[string]interface{}{} 563 for _, item := range field.Pairs { 564 key, err := strconv.Unquote(item.Key.String()) 565 if err != nil { 566 result[item.Key.String()] = CadenceValueToInterface(item.Value) 567 continue 568 } 569 570 result[key] = CadenceValueToInterface(item.Value) 571 } 572 return result 573 case cadence.Struct: 574 result := map[string]interface{}{} 575 subStructNames := field.StructType.Fields 576 577 for j, subField := range field.Fields { 578 result[subStructNames[j].Identifier] = CadenceValueToInterface(subField) 579 } 580 return result 581 case cadence.Array: 582 var result []interface{} 583 for _, item := range field.Values { 584 result = append(result, CadenceValueToInterface(item)) 585 } 586 return result 587 default: 588 result, err := strconv.Unquote(field.String()) 589 if err != nil { 590 return field.String() 591 } 592 return result 593 } 594 } 595 596 func FloatBalanceToUint(balance float64) uint64 { 597 return uint64(balance * 1000000) 598 }