1: <?php declare( strict_types = 1 );
2:
3: namespace Waves\API;
4:
5: use Exception;
6: use Waves\Common\ExceptionCode;
7: use Waves\Common\Json;
8: use Waves\Account\Address;
9: use Waves\Model\AssetId;
10: use Waves\Model\AssetDistribution;
11: use Waves\Model\AssetBalance;
12: use Waves\Model\AssetDetails;
13: use Waves\Model\Alias;
14: use Waves\Model\Balance;
15: use Waves\Model\BalanceDetails;
16: use Waves\Model\Block;
17: use Waves\Model\Id;
18: use Waves\Model\LeaseInfo;
19: use Waves\Model\BlockHeaders;
20: use Waves\Model\BlockchainRewards;
21: use Waves\Model\ChainId;
22: use Waves\Model\DataEntry;
23: use Waves\Model\HistoryBalance;
24: use Waves\Model\ScriptInfo;
25: use Waves\Model\ScriptMeta;
26: use Waves\Model\Status;
27: use Waves\Model\TransactionInfo;
28: use Waves\Model\TransactionStatus;
29: use Waves\Model\Validation;
30: use Waves\Transactions\Amount;
31: use Waves\Transactions\Transaction;
32:
33: class Node
34: {
35: const MAINNET = "https://nodes.wavesnodes.com";
36: const TESTNET = "https://nodes-testnet.wavesnodes.com";
37: const STAGENET = "https://nodes-stagenet.wavesnodes.com";
38: const LOCAL = "http://127.0.0.1:6869";
39:
40: private \deemru\WavesKit $wk;
41: private ChainId $chainId;
42: private string $uri;
43:
44: private string $wklevel = '';
45: private string $wkmessage = '';
46:
47: /**
48: * Creates Node instance
49: *
50: * @param string $uri Node REST API address
51: * @param ChainId $chainId Chain ID or "?" to set automatically (Default: "")
52: */
53: function __construct( string $uri, ChainId $chainId = null )
54: {
55: $this->uri = $uri;
56: $this->wk = new \deemru\WavesKit( '?', function( string $wklevel, string $wkmessage )
57: {
58: $this->wklevel = $wklevel;
59: $this->wkmessage = $wkmessage;
60: } );
61: $this->wk->setNodeAddress( $uri, 0 );
62:
63: if( !isset( $chainId ) )
64: {
65: if( $uri === Node::MAINNET )
66: $this->chainId = ChainId::MAINNET();
67: else
68: if( $uri === Node::TESTNET )
69: $this->chainId = ChainId::TESTNET();
70: else
71: if( $uri === Node::STAGENET )
72: $this->chainId = ChainId::STAGENET();
73: else
74: $this->chainId = $this->getAddresses()[0]->chainId();
75: }
76: else
77: {
78: $this->chainId = $chainId;
79: }
80:
81: $this->wk->chainId = $this->chainId->asString(); // @phpstan-ignore-line // accept workaround
82: }
83:
84: static function MAINNET(): Node
85: {
86: return new Node( Node::MAINNET );
87: }
88:
89: static function TESTNET(): Node
90: {
91: return new Node( Node::TESTNET );
92: }
93:
94: static function STAGENET(): Node
95: {
96: return new Node( Node::STAGENET );
97: }
98:
99: static function LOCAL(): Node
100: {
101: return new Node( Node::LOCAL );
102: }
103:
104: function chainId(): ChainId
105: {
106: return $this->chainId;
107: }
108:
109: function uri(): string
110: {
111: return $this->uri;
112: }
113:
114: /**
115: * Fetches a custom REST API request
116: *
117: * @param string $uri
118: * @param Json|string|null $data
119: * @return Json
120: */
121: private function fetch( string $uri, $data = null )
122: {
123: if( isset( $data ) )
124: {
125: if( is_string( $data ) )
126: $fetch = $this->wk->fetch( $uri, true, $data, null, [ 'Content-Type: text/plain', 'Accept: application/json' ] );
127: else
128: $fetch = $this->wk->fetch( $uri, true, $data->toString() );
129: }
130: else
131: $fetch = $this->wk->fetch( $uri );
132: if( $fetch === false )
133: {
134: $message = __FUNCTION__ . ' failed at `' . $uri . '`';
135: if( $this->wklevel === 'e' )
136: $message .= ' (' . $this->wkmessage . ')';
137: throw new Exception( $message, ExceptionCode::FETCH_URI );
138: }
139: $fetch = $this->wk->json_decode( $fetch );
140: if( $fetch === false )
141: throw new Exception( __FUNCTION__ . ' failed to decode `' . $uri . '`', ExceptionCode::JSON_DECODE );
142: return Json::as( $fetch );
143: }
144:
145: /**
146: * GETs a custom REST API request
147: *
148: * @param string $uri
149: * @return Json
150: */
151: function get( string $uri ): Json
152: {
153: return $this->fetch( $uri );
154: }
155:
156: /**
157: * POSTs a custom REST API request
158: *
159: * @param string $uri
160: * @param Json|string $data
161: * @return Json
162: */
163: function post( string $uri, $data ): Json
164: {
165: return $this->fetch( $uri, $data );
166: }
167:
168: //===============
169: // ADDRESSES
170: //===============
171:
172: /**
173: * Return addresses of the node
174: *
175: * @return array<int, Address>
176: */
177: function getAddresses(): array
178: {
179: return $this->get( '/addresses' )->asArrayAddress();
180: }
181:
182: /**
183: * Return addresses of the node by indexes
184: *
185: * @return array<int, Address>
186: */
187: function getAddressesByIndexes( int $fromIndex, int $toIndex ): array
188: {
189: return $this->get( '/addresses/seq/' . $fromIndex . '/' . $toIndex )->asArrayAddress();
190: }
191:
192: function getBalance( Address $address, int $confirmations = null ): int
193: {
194: $uri = '/addresses/balance/' . $address->toString();
195: if( isset( $confirmations ) )
196: $uri .= '/' . $confirmations;
197: return $this->get( $uri )->get( 'balance' )->asInt();
198: }
199:
200: /**
201: * Gets addresses balances
202: *
203: * @param array<int, Address> $addresses
204: * @param int|null $height (default: null)
205: * @return array<int, Balance>
206: */
207: function getBalances( array $addresses, int $height = null ): array
208: {
209: $json = Json::emptyJson();
210:
211: $array = [];
212: foreach( $addresses as $address )
213: $array[] = $address->toString();
214: $json->put( 'addresses', $array );
215:
216: if( isset( $height ) )
217: $json->put( 'height', $height );
218:
219: return $this->post( '/addresses/balance', $json )->asArrayBalance();
220: }
221:
222: function getBalanceDetails( Address $address ): BalanceDetails
223: {
224: return $this->get( '/addresses/balance/details/' . $address->toString() )->asBalanceDetails();
225: }
226:
227: /**
228: * Gets DataEntry array of address
229: *
230: * @param Address $address
231: * @param string|null $regex (default: null)
232: * @return array<int, DataEntry>
233: */
234: function getData( Address $address, string $regex = null ): array
235: {
236: $uri = '/addresses/data/' . $address->toString();
237: if( isset( $regex ) )
238: $uri .= '?matches=' . urlencode( $regex );
239: return $this->get( $uri )->asArrayDataEntry();
240: }
241:
242: /**
243: * Gets DataEntry array of address by keys
244: *
245: * @param Address $address
246: * @param array<int, string> $keys
247: * @return array<int, DataEntry>
248: */
249: function getDataByKeys( Address $address, array $keys ): array
250: {
251: $json = Json::emptyJson();
252:
253: $array = [];
254: foreach( $keys as $key )
255: $array[] = $key;
256: $json->put( 'keys', $array );
257:
258: return $this->post( '/addresses/data/' . $address->toString(), $json )->asArrayDataEntry();
259: }
260:
261: /**
262: * Gets a single DataEntry of address by a key
263: *
264: * @param Address $address
265: * @param string $key
266: * @return DataEntry
267: */
268: function getDataByKey( Address $address, string $key ): DataEntry
269: {
270: return $this->get( '/addresses/data/' . $address->toString() . '/' . $key )->asDataEntry();
271: }
272:
273: function getScriptInfo( Address $address ): ScriptInfo
274: {
275: return $this->get( '/addresses/scriptInfo/' . $address->toString() )->asScriptInfo();
276: }
277:
278: function getScriptMeta( Address $address ): ScriptMeta
279: {
280: $json = $this->get( '/addresses/scriptInfo/' . $address->toString() . '/meta' );
281: if( !$json->exists( 'meta' ) )
282: $json->put( 'meta', [ 'version' => 0, 'callableFuncTypes' => [] ] );
283: return $json->get( 'meta' )->asJson()->asScriptMeta();
284: }
285:
286: //===============
287: // ALIAS
288: //===============
289:
290: /**
291: * Gets an array of aliases by address
292: *
293: * @param Address $address
294: * @return array<int, Alias>
295: */
296: function getAliasesByAddress( Address $address ): array
297: {
298: return $this->get( '/alias/by-address/' . $address->toString() )->asArrayAlias();
299: }
300:
301: function getAddressByAlias( Alias $alias ): Address
302: {
303: return $this->get( '/alias/by-alias/' . $alias->name() )->get( 'address' )->asAddress();
304: }
305:
306: //===============
307: // ASSETS
308: //===============
309:
310: function getAssetDistribution( AssetId $assetId, int $height, int $limit = 1000, string $after = null ): AssetDistribution
311: {
312: $uri = '/assets/' . $assetId->toString() . '/distribution/' . $height . '/limit/' . $limit;
313: if( isset( $after ) )
314: $uri .= '?after=' . $after;
315: return $this->get( $uri )->asAssetDistribution();
316: }
317:
318: /**
319: * Gets an array of AssetBalance for an address
320: *
321: * @param Address $address
322: * @return array<int, AssetBalance>
323: */
324: function getAssetsBalance( Address $address ): array
325: {
326: return $this->get( '/assets/balance/' . $address->toString() )->get( 'balances' )->asJson()->asArrayAssetBalance();
327: }
328:
329: function getAssetBalance( Address $address, AssetId $assetId ): int
330: {
331: return $assetId->isWaves() ?
332: $this->getBalance( $address ) :
333: $this->get( '/assets/balance/' . $address->toString() . '/' . $assetId->toString() )->get( 'balance' )->asInt();
334: }
335:
336: function getAssetDetails( AssetId $assetId ): AssetDetails
337: {
338: return $this->get( '/assets/details/' . $assetId->toString() . '?full=true' )->asAssetDetails();
339: }
340:
341: /**
342: * @param array<int, AssetId> $assetIds
343: * @return array<int, AssetDetails>
344: */
345: function getAssetsDetails( array $assetIds ): array
346: {
347: $json = Json::emptyJson();
348:
349: $array = [];
350: foreach( $assetIds as $assetId )
351: $array[] = $assetId->toString();
352: $json->put( 'ids', $array );
353:
354: return $this->post( '/assets/details?full=true', $json )->asArrayAssetDetails();
355: }
356:
357: /**
358: * @return array<int, AssetDetails>
359: */
360: function getNft( Address $address, int $limit = 1000, AssetId $after = null ): array
361: {
362: $uri = '/assets/nft/' . $address->toString() . '/limit/' . $limit;
363: if( isset( $after ) )
364: $uri .= '?after=' . $after->toString();
365: return $this->get( $uri )->asArrayAssetDetails();
366: }
367:
368: //===============
369: // BLOCKCHAIN
370: //===============
371:
372: function getBlockchainRewards( int $height = null ): BlockchainRewards
373: {
374: $uri = '/blockchain/rewards';
375: if( isset( $height ) )
376: $uri .= '/' . $height;
377: return $this->get( $uri )->asBlockchainRewards();
378: }
379:
380: //===============
381: // BLOCKS
382: //===============
383:
384: function getHeight(): int
385: {
386: return $this->get( '/blocks/height' )->get( 'height' )->asInt();
387: }
388:
389: function getBlockHeightById( string $blockId ): int
390: {
391: return $this->get( '/blocks/height/' . $blockId )->get( 'height' )->asInt();
392: }
393:
394: function getBlockHeightByTimestamp( int $timestamp ): int
395: {
396: return $this->get( "/blocks/heightByTimestamp/" . $timestamp )->get( "height" )->asInt();
397: }
398:
399: function getBlocksDelay( string $startBlockId, int $blocksNum ): int
400: {
401: return $this->get( "/blocks/delay/" . $startBlockId . "/" . $blocksNum )->get( "delay" )->asInt();
402: }
403:
404: function getBlockHeadersByHeight( int $height ): BlockHeaders
405: {
406: return $this->get( "/blocks/headers/at/" . $height )->asBlockHeaders();
407: }
408:
409: function getBlockHeadersById( string $blockId ): BlockHeaders
410: {
411: return $this->get( "/blocks/headers/" . $blockId )->asBlockHeaders();
412: }
413:
414: /**
415: * Get an array of BlockHeaders from fromHeight to toHeight
416: *
417: * @param integer $fromHeight
418: * @param integer $toHeight
419: * @return array<int, BlockHeaders>
420: */
421: function getBlocksHeaders( int $fromHeight, int $toHeight ): array
422: {
423: return $this->get( "/blocks/headers/seq/" . $fromHeight . "/" . $toHeight )->asArrayBlockHeaders();
424: }
425:
426: function getLastBlockHeaders(): BlockHeaders
427: {
428: return $this->get( "/blocks/headers/last" )->asBlockHeaders();
429: }
430:
431: function getBlockByHeight( int $height ): Block
432: {
433: return $this->get( '/blocks/at/' . $height )->asBlock();
434: }
435:
436: function getBlockById( Id $id ): Block
437: {
438: return $this->get( '/blocks/' . $id->toString() )->asBlock();
439: }
440:
441: /**
442: * @return array<int, Block>
443: */
444: function getBlocks( int $fromHeight, int $toHeight ): array
445: {
446: return $this->get( '/blocks/seq/' . $fromHeight . '/' . $toHeight )->asArrayBlock();
447: }
448:
449: function getGenesisBlock(): Block
450: {
451: return $this->get( '/blocks/first' )->asBlock();
452: }
453:
454: function getLastBlock(): Block
455: {
456: return $this->get( '/blocks/last' )->asBlock();
457: }
458:
459: /**
460: * @return array<int, Block>
461: */
462: function getBlocksGeneratedBy( Address $generator, int $fromHeight, int $toHeight ): array
463: {
464: return $this->get( '/blocks/address/' . $generator->toString() . '/' . $fromHeight . '/' . $toHeight )->asArrayBlock();
465: }
466:
467: //===============
468: // NODE
469: //===============
470:
471: function getVersion(): string
472: {
473: return $this->get( '/node/version')->get( 'version' )->asString();
474: }
475:
476: //===============
477: // DEBUG
478: //===============
479:
480: /**
481: * @param Address $address
482: * @return array<int, HistoryBalance>
483: */
484: function getBalanceHistory( Address $address ): array
485: {
486: return $this->get( '/debug/balances/history/' . $address->toString() )->asArrayHistoryBalance();
487: }
488:
489: function validateTransaction( Transaction $transaction ): Validation
490: {
491: return $this->post( '/debug/validate', $transaction->json() )->asValidation();
492: }
493:
494: //===============
495: // LEASING
496: //===============
497:
498: /**
499: * @return array<int, LeaseInfo>
500: */
501: function getActiveLeases( Address $address ): array
502: {
503: return $this->get( '/leasing/active/' . $address->toString() )->asArrayLeaseInfo();
504: }
505:
506: function getLeaseInfo( Id $leaseId ): LeaseInfo
507: {
508: return $this->get( '/leasing/info/' . $leaseId->toString() )->asLeaseInfo();
509: }
510:
511: /**
512: * @param array<int, Id> $leaseIds
513: * @return array<int, LeaseInfo>
514: */
515: function getLeasesInfo( array $leaseIds ): array
516: {
517: $json = Json::emptyJson();
518:
519: $array = [];
520: foreach( $leaseIds as $leaseId )
521: $array[] = $leaseId->toString();
522: $json->put( 'ids', $array );
523:
524: return $this->post( '/leasing/info', $json )->asArrayLeaseInfo();
525: }
526:
527: //===============
528: // TRANSACTIONS
529: //===============
530:
531: function calculateTransactionFee( Transaction $transaction ): Amount
532: {
533: $json = $this->post( '/transactions/calculateFee', $transaction->json() );
534: return Amount::fromJson( $json, 'feeAmount', 'feeAssetId' );
535: }
536:
537: function serializeTransaction( Transaction $transaction ): string
538: {
539: $json = $this->post( '/utils/transactionSerialize', $transaction->json() );
540: $bytes = '';
541: foreach( $json->get( 'bytes' )->asArrayInt() as $byte )
542: $bytes .= chr( $byte );
543: return $bytes;
544: }
545:
546: function broadcast( Transaction $transaction ): Transaction
547: {
548: return $this->post( '/transactions/broadcast', $transaction->json() )->asTransaction();
549: }
550:
551: function getTransactionInfo( Id $txId ): TransactionInfo
552: {
553: return $this->get( '/transactions/info/' . $txId->toString() )->asTransactionInfo();
554: }
555:
556: /**
557: * @return array<int, TransactionInfo>
558: */
559: function getTransactionsByAddress( Address $address, int $limit = 100, Id $afterTxId = null ): array
560: {
561: $uri = '/transactions/address/' . $address->toString() . '/limit/' . $limit;
562: if( isset( $afterTxId ) )
563: $uri .= '?after=' . $afterTxId->toString();
564: return $this->get( $uri )->get( 0 )->asJson()->asArrayTransactionInfo();
565: }
566:
567: function getTransactionStatus( Id $txId ): TransactionStatus
568: {
569: return $this->get( '/transactions/status?id=' . $txId->toString() )->get( 0 )->asJson()->asTransactionStatus();
570: }
571:
572: /**
573: * @param array<int, Id> $txIds
574: * @return array<int, TransactionStatus>
575: */
576: function getTransactionsStatus( array $txIds ): array
577: {
578: $json = Json::emptyJson();
579:
580: $array = [];
581: foreach( $txIds as $txId )
582: $array[] = $txId->toString();
583: $json->put( 'ids', $array );
584:
585: return $this->post( '/transactions/status', $json )->asArrayTransactionStatus();
586: }
587:
588: function getUnconfirmedTransaction( Id $txId ): Transaction
589: {
590: return $this->get( '/transactions/unconfirmed/info/' . $txId->toString() )->asTransaction();
591: }
592:
593: /**
594: * @return array<int, Transaction>
595: */
596: function getUnconfirmedTransactions(): array
597: {
598: return $this->get( '/transactions/unconfirmed' )->asArrayTransaction();
599: }
600:
601: function getUtxSize(): int
602: {
603: return $this->get( '/transactions/unconfirmed/size' )->get( 'size' )->asInt();
604: }
605:
606: //===============
607: // UTILS
608: //===============
609:
610: function compileScript( string $source, bool $enableCompaction = null ): ScriptInfo
611: {
612: $uri = '/utils/script/compileCode';
613: if( isset( $enableCompaction ) )
614: $uri .= '?compact=' . ( $enableCompaction ? 'true' : 'false' );
615: return $this->post( $uri, $source )->asScriptInfo();
616: }
617:
618: function ethToWavesAsset( string $asset ): string
619: {
620: return $this->get( '/eth/assets?id=' . $asset )->get( 0 )->asJson()->asAssetDetails()->assetId()->encoded();
621: }
622:
623: //===============
624: // WAITINGS
625: //===============
626:
627: const blockInterval = 60;
628:
629: function waitForTransaction( Id $id, int $waitingInSeconds = Node::blockInterval ): TransactionInfo
630: {
631: if( $waitingInSeconds < 1 )
632: $waitingInSeconds = 1;
633:
634: $pollingIntervalInMillis = 100;
635: $pollingIntervalInMicros = $pollingIntervalInMillis * 1000;
636: $waitingInMillis = $waitingInSeconds * 1000;
637:
638: for( $spentMillis = 0; $spentMillis < $waitingInMillis; $spentMillis += $pollingIntervalInMillis )
639: {
640: try
641: {
642: return $this->getTransactionInfo( $id );
643: }
644: catch( Exception $e )
645: {
646: if( $e->getCode() !== ExceptionCode::FETCH_URI )
647: throw new Exception( __FUNCTION__ . ' unexpected exception `' . $e->getCode() . '`:`' . $e->getMessage() . '`', ExceptionCode::UNEXPECTED );
648:
649: usleep( $pollingIntervalInMicros );
650: }
651: }
652:
653: throw new Exception( __FUNCTION__ . ' could not wait for transaction `' . $id->toString() . '` in ' . $waitingInSeconds . ' seconds', ExceptionCode::TIMEOUT );
654: }
655:
656: /**
657: * @param array<int, Id> $ids
658: * @param int $waitingInSeconds
659: * @return void
660: */
661: function waitForTransactions( array $ids, int $waitingInSeconds = Node::blockInterval ): void
662: {
663: if( $waitingInSeconds < 1 )
664: $waitingInSeconds = 1;
665:
666: $pollingIntervalInMillis = 1000;
667: $pollingIntervalInMicros = $pollingIntervalInMillis * 1000;
668: $waitingInMillis = $waitingInSeconds * 1000;
669:
670: for( $spentMillis = 0; $spentMillis < $waitingInMillis; $spentMillis += $pollingIntervalInMillis )
671: {
672: try
673: {
674: $isOK = true;
675: $statuses = $this->getTransactionsStatus( $ids );
676: foreach( $statuses as $status )
677: if( $status->status() !== Status::CONFIRMED )
678: {
679: $isOK = false;
680: break;
681: }
682:
683: if( $isOK )
684: return;
685:
686: usleep( $pollingIntervalInMicros );
687: }
688: catch( Exception $e )
689: {
690: if( $e->getCode() !== ExceptionCode::FETCH_URI )
691: throw new Exception( __FUNCTION__ . ' unexpected exception `' . $e->getCode() . '`:`' . $e->getMessage() . '`', ExceptionCode::UNEXPECTED );
692:
693: usleep( $pollingIntervalInMicros );
694: }
695: }
696:
697: throw new Exception( __FUNCTION__ . ' could not wait for transactions', ExceptionCode::TIMEOUT );
698: }
699:
700: function waitForHeight( int $target, int $waitingInSeconds = Node::blockInterval * 3 ): int
701: {
702: $start = $this->getHeight();
703: $prev = $start;
704:
705: if( $waitingInSeconds < 1 )
706: $waitingInSeconds = 1;
707:
708: $pollingIntervalInMillis = 100;
709: $pollingIntervalInMicros = $pollingIntervalInMillis * 1000;
710: $waitingInMillis = $waitingInSeconds * 1000;
711:
712: $current = $start;
713: for( $spentMillis = 0; $spentMillis < $waitingInMillis; $spentMillis += $pollingIntervalInMillis )
714: {
715: if( $current >= $target )
716: return $current;
717: else if( $current > $prev )
718: {
719: $prev = $current;
720: $spentMillis = 0;
721: }
722:
723: usleep( $pollingIntervalInMicros );
724: $current = $this->getHeight();
725: }
726:
727: throw new Exception( __FUNCTION__ . ' could not wait for height `' . $target . '` in ' . $waitingInSeconds . ' seconds', ExceptionCode::TIMEOUT );
728: }
729:
730: function waitBlocks( int $blocksCount, int $waitingInSeconds = Node::blockInterval * 3 ): int
731: {
732: return $this->waitForHeight( $this->getHeight() + $blocksCount, $waitingInSeconds );
733: }
734: }