Skip to content

Commit

Permalink
Remit (XLS-55) support
Browse files Browse the repository at this point in the history
  • Loading branch information
zgrguric committed Mar 16, 2024
1 parent 855b171 commit dd6ce38
Show file tree
Hide file tree
Showing 7 changed files with 449 additions and 11 deletions.
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@

# XRPL NFT Transaction Mutation Parser for PHP

## Demo
## Supporting networks

See this in action on [XRPLWin Playground](https://playground.xrpl.win/play/xrpl-nft-transaction-mutation-parser)
- XRPL
- Xahau

## Description

Parses NFToken and URIToken (referred as NFT) Transactions (`NFTokenMint`, `URITokenMint`, `NFTokenBurn`, `URITokenBurn`, `NFTokenAcceptOffer`, `URITokenBuy`, `NFTokenCancelOffer`, `URITokenCancelSellOffer`, `NFTokenCreateOffer`, `URITokenCreateSellOffer`) with account context and returns affected NFT, direction that NFT was transferred, minted or destroyed, and outputs roles referencing account has in specific transaction.
Parses NFToken and URIToken (referred as NFT) Transactions (`NFTokenMint`, `URITokenMint`, `NFTokenBurn`, `URITokenBurn`, `NFTokenAcceptOffer`, `URITokenBuy`, `NFTokenCancelOffer`, `URITokenCancelSellOffer`, `NFTokenCreateOffer`, `URITokenCreateSellOffer`, `Remit`) with account context and returns affected NFT, direction that NFT was transferred, minted or destroyed, and outputs roles referencing account has in specific transaction.

With this parser you can find out what has happened with referencing account after transaction was executed. For example when token is minted - parser will output token ID and direction IN, this means referenced account was minter and new token is added to reference account ownership.

**Remit (XLS-55)**
Remit Transaction Type can mint a single URIToken which is present in this transaction like any other NFT. Additionally Remit can transfer none, one or more existing URIToken-s from Account to Destination, those tokens are present in `nfts` and `ref.nfts` array key, sending Account has role 'SELLER' and receiver Destination has role 'OWNER'.

What is checked:

- **Token id** - affected token ID in question
- **Token direction** - minted - IN, burned, OUT, sold - OUT, bought - IN
- **Roles** - role of referencing account in this transaction, is it minter, burner, seller, buyer, broker, or issuer
- **Remitted URITokens** - list of tokens transferred in `Remit` transaction type

Note about issuer:
Note about NFToken (XLS-20) issuer:
Issuer can only happen in `NFTokenAcceptOffer` transaction type, it is extracted from modified AccountRoot node by checking if balance has been changed. If yes then this account gained percentage of sale, and it is issuer of NFToken.

### Note
Expand Down Expand Up @@ -61,19 +66,19 @@ print_r($parsedTransaction);
├ Output for $parsedTransaction:
├ Array (
├ [nft] => 00082710...
├ [nfts] => []
├ [context] => null
├ [ref] => Array
├ (
├ [account] => rAbc...
├ [nft] => 00082710...
├ [nfts] => []
├ [direction] => IN
├ [roles] => Array
├ (
├ [0] => OWNER
├ )
├ )
├ )
*/
Expand All @@ -83,10 +88,12 @@ print_r($parsedTransaction);

| Key | Type | Description |
| ------------- | ------------- | ------------- |
| nft | ?String | NFTokenID or URIToken always present in types: `NFTokenMint`, `NFTokenBurn`, `NFTokenAcceptOffer`, `NFTokenCreateOffer`, `URI*` |
| nft | ?String | NFToken or URIToken always present in types: `NFTokenMint`, `NFTokenBurn`, `NFTokenAcceptOffer`, `NFTokenCreateOffer`, `URI*`, `Remit` |
| nfts | Array | NFTokens transferred in Remit transaction type (not including Minted token) |
| context | ?String | Context of transaction (specifically offers). One of: `null`,`"BUY"`,`"SELL"`,`"BROKERED"` |
| ref.account | String | Reference account |
| ref.nft | ?String | NFTokenID or URIToken which changed ownership depending on direction for reference account |
| ref.nft | ?String | NFTokenID or URIToken which changed ownership depending on direction for reference account including minted URIToken in Remit |
| ref.nfts | Array | URITokens which changed ownership depending on direction for reference account in Remit transaction only, NFTokens transferred in Remit transaction type (not including Minted token) |
| ref.direction | String | One of: `"IN"`,`"OUT"`,`"UNKNOWN"` |
| ref.roles | Array | Array of roles reference account has in this transaction, possible roles: `"UNKNOWN"`, `"OWNER"`, `"MINTER"`, `"BURNER"`, `"BUYER"`, `"SELLER"`, `"BROKER"`, `"ISSUER"` |

Expand All @@ -99,3 +106,7 @@ or
```
./vendor/bin/phpunit --testdox
```

## Demo

See this in action on [XRPLWin Playground](https://playground.xrpl.win/play/xrpl-nft-transaction-mutation-parser)
72 changes: 70 additions & 2 deletions src/NFTTxMutationParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ class NFTTxMutationParser

# Result variables
private ?string $nft = null;
private array $nfts = []; //Remit transfers
private ?string $context = null;

# Reference account result variables
private ?string $ref_nft = null;
private array $ref_nfts = []; //Remit transfers
private string $ref_direction = self::DIRECTION_UNKNOWN;
private array $ref_roles = [];

Expand Down Expand Up @@ -88,6 +90,9 @@ public function __construct(string $reference_account, \stdClass $tx)
case 'URITokenCancelSellOffer':
$this->handleURITokenCancelSellOffer();
break;
case 'Remit':
$this->handleRemit();
break;
}
$this->nft = $this->ref_nft;

Expand Down Expand Up @@ -116,6 +121,10 @@ public function __construct(string $reference_account, \stdClass $tx)
case 'URITokenCancelSellOffer':
$this->nft = $this->extractAffectedURITokenID();
break;
case 'Remit':
$this->nft = $this->extractAffectedURITokenID(false);
$this->nfts = $this->extractRemitURITokenIDs();
break;
}
}
}
Expand Down Expand Up @@ -431,13 +440,68 @@ private function handleURITokenCancelSellOffer(): void
}
}

private function handleRemit(): void
{

//if(!isset($this->tx->MintURIToken))
// return;

if($this->account == $this->tx->Account) {
// Remit creator perspective
$nft = $this->extractAffectedURITokenID(false);
$ref_nfts = $this->extractRemitURITokenIDs();
if($nft == null && !count($ref_nfts)) return; //no affected nfts found

$this->ref_direction = self::DIRECTION_OUT;
if($nft != null) {
//Minted nft found
$this->ref_roles = [self::ROLE_ISSUER, self::ROLE_MINTER];
$this->ref_nft = $nft;
}

if(count($ref_nfts)) {
//NFT Transfers found
$this->ref_roles[] = self::ROLE_SELLER;
$this->ref_nfts = $ref_nfts;
}

} else if ($this->account == $this->tx->Destination) {
//Remit receiver perspective
$nft = $this->extractAffectedURITokenID(false);
$ref_nfts = $this->extractRemitURITokenIDs();
if($nft == null && !count($ref_nfts)) return; //no affected nfts found

$this->ref_direction = self::DIRECTION_IN;
if($nft != null) {
//Minted nft found
$this->ref_roles = [self::ROLE_OWNER];
$this->ref_nft = $nft;
}

if(count($ref_nfts)) {
//NFT Transfers found
$this->ref_roles[] = self::ROLE_OWNER;
$this->ref_nfts = $ref_nfts;
}
}
$this->ref_roles = \array_unique($this->ref_roles);
}


private function extractRemitURITokenIDs(): array
{
if(!isset($this->tx->URITokenIDs))
return [];
return $this->tx->URITokenIDs;
}

/**
* Extracts single URITokenID from metadata
* Handled CreatedNode of type URIToken
* @throws \Exception
* @return string
*/
private function extractAffectedURITokenID(): string
private function extractAffectedURITokenID(bool $throwOnEmpty = true): ?string
{
if(isset($this->tx->URITokenID))
return $this->tx->URITokenID;
Expand All @@ -447,7 +511,9 @@ private function extractAffectedURITokenID(): string
return $an->CreatedNode->LedgerIndex;
}
}
throw new \Exception('Unhandled: no URITokenID found in meta in tx ['.$this->tx->hash.']');
if($throwOnEmpty)
throw new \Exception('Unhandled: no URITokenID found in meta in tx ['.$this->tx->hash.']');
return null;
}

/**
Expand Down Expand Up @@ -610,10 +676,12 @@ public function result(): array
\sort($roles,SORT_REGULAR);
return [
'nft' => $this->nft,
'nfts' => $this->nfts,
'context' => $this->context,
'ref' => [
'account' => $this->account,
'nft' => $this->ref_nft,
'nfts' => $this->ref_nfts,
'direction' => $this->ref_direction,
'roles' => $roles
]
Expand Down
2 changes: 1 addition & 1 deletion tests/Tx27Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function testUriTokenDestination()
$account = "rnBA8kVE8ZxqiRnUccKEo2x1Qa6sJWDXA9";
$NFTTxMutationParser = new NFTTxMutationParser($account, $transaction->result);
$parsedTransaction = $NFTTxMutationParser->result();
//dd($parsedTransaction);

$this->assertIsArray($parsedTransaction);

$this->assertEquals('39C0D52EDEF285103DF8A9CCE6F4E1A4AE206A76D46BA2AF4834135856E36840',$parsedTransaction['nft']);
Expand Down
59 changes: 59 additions & 0 deletions tests/Tx28Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types=1);

namespace XRPLWin\XRPLNFTTxMutatationParser\Tests;

use PHPUnit\Framework\TestCase;
use XRPLWin\XRPLNFTTxMutatationParser\NFTTxMutationParser;

/***
* Remit
* Mint URIToken
*/
final class Tx28Test extends TestCase
{
public function testRemitSenderMinter()
{
$transaction = file_get_contents(__DIR__.'/fixtures/tx28.json');
$transaction = \json_decode($transaction);
$account = "r3RtnU293vBgLCHvCRmo2goUECnnMVS5qA"; //minter
$NFTTxMutationParser = new NFTTxMutationParser($account, $transaction->result);
$parsedTransaction = $NFTTxMutationParser->result();

$this->assertIsArray($parsedTransaction);

$this->assertEquals('80B3BD46EBBFDFD0317CCFE7F533988A73A4281FABF1289242D00F8ED1C61878',$parsedTransaction['nft']);
$this->assertEquals('80B3BD46EBBFDFD0317CCFE7F533988A73A4281FABF1289242D00F8ED1C61878',$parsedTransaction['ref']['nft']);
$this->assertEquals('OUT',$parsedTransaction['ref']['direction']);
$this->assertEquals(['ISSUER','MINTER'],$parsedTransaction['ref']['roles']);
}

public function testRemitDestination()
{
$transaction = file_get_contents(__DIR__.'/fixtures/tx28.json');
$transaction = \json_decode($transaction);
$account = "rBL1AMHX2J1uoMKZqpViHSBBcMbLpGpf9i";
$NFTTxMutationParser = new NFTTxMutationParser($account, $transaction->result);
$parsedTransaction = $NFTTxMutationParser->result();
$this->assertIsArray($parsedTransaction);

$this->assertEquals('80B3BD46EBBFDFD0317CCFE7F533988A73A4281FABF1289242D00F8ED1C61878',$parsedTransaction['nft']);
$this->assertEquals('80B3BD46EBBFDFD0317CCFE7F533988A73A4281FABF1289242D00F8ED1C61878',$parsedTransaction['ref']['nft']);
$this->assertEquals('IN',$parsedTransaction['ref']['direction']);
$this->assertEquals(['OWNER'],$parsedTransaction['ref']['roles']);
}

public function testRemitOther()
{
$transaction = file_get_contents(__DIR__.'/fixtures/tx28.json');
$transaction = \json_decode($transaction);
$account = "r9gYbjBfANRfA1JHfaCVfPPGfXYiqQvmhS";
$NFTTxMutationParser = new NFTTxMutationParser($account, $transaction->result);
$parsedTransaction = $NFTTxMutationParser->result();

$this->assertIsArray($parsedTransaction);
$this->assertEquals('80B3BD46EBBFDFD0317CCFE7F533988A73A4281FABF1289242D00F8ED1C61878',$parsedTransaction['nft']);
$this->assertEquals(null,$parsedTransaction['ref']['nft']);
$this->assertEquals('UNKNOWN',$parsedTransaction['ref']['direction']);
$this->assertEquals(['UNKNOWN'],$parsedTransaction['ref']['roles']);
}
}
86 changes: 86 additions & 0 deletions tests/Tx29Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php declare(strict_types=1);

namespace XRPLWin\XRPLNFTTxMutatationParser\Tests;

use PHPUnit\Framework\TestCase;
use XRPLWin\XRPLNFTTxMutatationParser\NFTTxMutationParser;

/***
* Remit
* Transfer two URITokens
*/
final class Tx29Test extends TestCase
{
public function testRemitSenderMinter()
{
$transaction = file_get_contents(__DIR__.'/fixtures/tx29.json');
$transaction = \json_decode($transaction);
$account = "r3RtnU293vBgLCHvCRmo2goUECnnMVS5qA"; //minter
$NFTTxMutationParser = new NFTTxMutationParser($account, $transaction->result);
$parsedTransaction = $NFTTxMutationParser->result();

$this->assertIsArray($parsedTransaction);
$this->assertEquals(null,$parsedTransaction['nft']);
$this->assertEquals(null,$parsedTransaction['ref']['nft']);
$this->assertEquals('OUT',$parsedTransaction['ref']['direction']);
$this->assertEquals(['SELLER'],$parsedTransaction['ref']['roles']);

$this->assertIsArray($parsedTransaction['nfts']);
$this->assertEquals([
'38B40CB673CB072DEBB7FBE798162570B081BA6E89652BF2FDA0C522A1612920',
'02E51D07084FA44F7D7B521EE1F0439E7B4FB0ADD98F129C8FDDEA643A9ABCC7'
],$parsedTransaction['nfts']);
$this->assertEquals([
'38B40CB673CB072DEBB7FBE798162570B081BA6E89652BF2FDA0C522A1612920',
'02E51D07084FA44F7D7B521EE1F0439E7B4FB0ADD98F129C8FDDEA643A9ABCC7'
],$parsedTransaction['ref']['nfts']);
}

public function testRemitDestination()
{
$transaction = file_get_contents(__DIR__.'/fixtures/tx29.json');
$transaction = \json_decode($transaction);
$account = "rBL1AMHX2J1uoMKZqpViHSBBcMbLpGpf9i";
$NFTTxMutationParser = new NFTTxMutationParser($account, $transaction->result);
$parsedTransaction = $NFTTxMutationParser->result();
$this->assertIsArray($parsedTransaction);

$this->assertIsArray($parsedTransaction);
$this->assertEquals(null,$parsedTransaction['nft']);
$this->assertEquals(null,$parsedTransaction['ref']['nft']);
$this->assertEquals('IN',$parsedTransaction['ref']['direction']);
$this->assertEquals(['OWNER'],$parsedTransaction['ref']['roles']);

$this->assertIsArray($parsedTransaction['nfts']);
$this->assertEquals([
'38B40CB673CB072DEBB7FBE798162570B081BA6E89652BF2FDA0C522A1612920',
'02E51D07084FA44F7D7B521EE1F0439E7B4FB0ADD98F129C8FDDEA643A9ABCC7'
],$parsedTransaction['nfts']);
$this->assertEquals([
'38B40CB673CB072DEBB7FBE798162570B081BA6E89652BF2FDA0C522A1612920',
'02E51D07084FA44F7D7B521EE1F0439E7B4FB0ADD98F129C8FDDEA643A9ABCC7'
],$parsedTransaction['ref']['nfts']);
}

public function testRemitOther()
{
$transaction = file_get_contents(__DIR__.'/fixtures/tx29.json');
$transaction = \json_decode($transaction);
$account = "r9gYbjBfANRfA1JHfaCVfPPGfXYiqQvmhS";
$NFTTxMutationParser = new NFTTxMutationParser($account, $transaction->result);
$parsedTransaction = $NFTTxMutationParser->result();

$this->assertIsArray($parsedTransaction);
$this->assertEquals(null,$parsedTransaction['nft']);
$this->assertEquals(null,$parsedTransaction['ref']['nft']);
$this->assertEquals('UNKNOWN',$parsedTransaction['ref']['direction']);
$this->assertEquals(['UNKNOWN'],$parsedTransaction['ref']['roles']);
$this->assertIsArray($parsedTransaction['nfts']);
$this->assertEquals([
'38B40CB673CB072DEBB7FBE798162570B081BA6E89652BF2FDA0C522A1612920',
'02E51D07084FA44F7D7B521EE1F0439E7B4FB0ADD98F129C8FDDEA643A9ABCC7'
],$parsedTransaction['nfts']);
$this->assertEquals([],$parsedTransaction['ref']['nfts']);
}

}
Loading

0 comments on commit dd6ce38

Please sign in to comment.