Skip to content

Commit

Permalink
[PLA-1939] Refactor max token count and supply rule (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
enjinabner committed Sep 5, 2024
1 parent 38097cc commit 148e7cb
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 159 deletions.
2 changes: 1 addition & 1 deletion lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
'verify_signed_message' => 'The :attribute is invalid.',
'beam_scan_not_found' => 'Beam scan record is not found.',
'max_token_count' => 'The token count exceeded the maximum limit of :limit for this collection.',
'max_token_supply' => 'The :attribute exceeded the maximum supply limit of :limit for each token for this collection.',
'max_token_supply' => 'The :attribute exceeded the maximum supply limit of :limit for unique tokens for this collection.',
'has_beam_flag' => 'The :attribute is invalid.',
'not_expired' => 'The beam has expired.',
'tokens_doesnt_exist_in_beam' => 'The :attribute already exist in beam.',
Expand Down
119 changes: 94 additions & 25 deletions src/Rules/MaxTokenCount.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
use Enjin\Platform\Beam\Models\BeamClaim;
use Enjin\Platform\Beam\Rules\Traits\IntegerRange;
use Enjin\Platform\Models\Collection;
use Enjin\Platform\Models\Token;
use Enjin\Platform\Rules\Traits\HasDataAwareRule;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Arr;
use Illuminate\Support\LazyCollection;

class MaxTokenCount implements DataAwareRule, ValidationRule
{
Expand Down Expand Up @@ -39,35 +41,102 @@ public function __construct(protected ?string $collectionId)
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($this->collectionId && ($collection = Collection::withCount('tokens')->firstWhere(['collection_chain_id'=> $this->collectionId]))) {
if (!is_null($this->limit = $collection->max_token_count)) {
$passes = $collection->max_token_count >= $collection->tokens_count
+ collect($this->data['tokens'])
->filter(fn ($token) => BeamType::getEnumCase($token['type']) == BeamType::MINT_ON_DEMAND)
->reduce(function ($carry, $token) {
return collect(Arr::get($token, 'tokenIds'))->reduce(function ($val, $tokenId) use ($token) {
$range = $this->integerRange($tokenId);

return $val + (
$range === false
? $token['claimQuantity']
: (($range[1] - $range[0]) + 1) * $token['claimQuantity']
);
}, $carry);
}, 0)
+ BeamClaim::whereHas(
'beam',
fn ($query) => $query->where('collection_chain_id', $this->collectionId)->where('end', '>', now())
)->where('type', BeamType::MINT_ON_DEMAND->name)
/**
* The sum of all unique tokens (including existing tokens, tokens in beams, and tokens to be created)
* must not exceed the collection's maximum token count.
*/
if ($this->collectionId
&& ($collection = Collection::withCount('tokens')->firstWhere(['collection_chain_id' => $this->collectionId]))
&& ! is_null($this->limit = $collection->max_token_count)
) {
if ($this->limit == 0) {
$fail('enjin-platform-beam::validation.max_token_count')->translate(['limit' => $this->limit]);

return;
}

$claimCount = BeamClaim::where('type', BeamType::MINT_ON_DEMAND->name)
->whereHas(
'beam',
fn ($query) => $query->where('collection_chain_id', $this->collectionId)->where('end', '>', now())
)->whereNotExists(function ($query) {
$query->selectRaw('1')
->from('tokens')
->whereColumn('tokens.token_chain_id', 'beam_claims.token_chain_id');
})
->groupBy('token_chain_id')
->count();

$tokens = collect($this->data['tokens'])
->filter(fn ($data) => !empty(Arr::get($data, 'tokenIds')))
->pluck('tokenIds')
->flatten();

collect($this->data['tokens'])
->filter(fn ($data) => !empty(Arr::get($data, 'tokenIdDataUpload')))
->map(function ($data) use ($tokens) {
$handle = fopen($data['tokenIdDataUpload']->getPathname(), 'r');
while (($line = fgets($handle)) !== false) {
if (! $this->tokenIdExists($tokens->all(), $tokenId = trim($line))) {
$tokens->push($tokenId);
}
}
fclose($handle);
});

[$integers, $ranges] = collect($tokens)->unique()->partition(fn ($val) => $this->integerRange($val) === false);

$createTokenTotal = 0;
if ($integers->count()) {
$existingTokens = Token::where('collection_id', $collection->id)
->whereIn('token_chain_id', $integers)
->pluck('token_chain_id');

$integers = $integers->diff($existingTokens);
if ($integers->count()) {
$existingClaimsCount = BeamClaim::where('collection_id', $collection->id)
->whereIn('token_chain_id', $integers)
->claimable()
->pluck('token_chain_id');

$createTokenTotal = $integers->diff($existingClaimsCount)->count();
}
}

if ($ranges->count()) {
foreach ($ranges as $range) {
[$from, $to] = $this->integerRange($range);
$existingTokensCount = Token::where('collection_id', $collection->id)
->whereBetween('token_chain_id', [(int) $from, (int) $to])
->count();

if (!$passes) {
$fail('enjin-platform-beam::validation.max_token_count')
->translate([
'limit' => $this->limit,
]);
if (($to - $from) + 1 == $existingTokensCount) {
continue;
}

LazyCollection::range((int) $from, (int) $to)
->chunk(5000)
->each(function ($chunk) use (&$createTokenTotal, $collection) {
$existingTokens = Token::where('collection_id', $collection->id)
->whereIn('token_chain_id', $chunk)
->pluck('token_chain_id');

$integers = $chunk->diff($existingTokens);
if ($integers->count()) {
$existingClaimsCount = BeamClaim::where('collection_id', $collection->id)
->whereIn('token_chain_id', $integers)
->claimable()
->pluck('token_chain_id');
$createTokenTotal += $integers->diff($existingClaimsCount)->count();
}
});
}
}

$createTokenTotal = $createTokenTotal > 0 ? $createTokenTotal : 0;
if ($collection->max_token_count < $collection->tokens_count + $claimCount + $createTokenTotal) {
$fail('enjin-platform-beam::validation.max_token_count')->translate(['limit' => $this->limit]);
}
}
}
}
145 changes: 62 additions & 83 deletions src/Rules/MaxTokenSupply.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
use Enjin\Platform\Beam\Rules\Traits\IntegerRange;
use Enjin\Platform\Models\Collection;
use Enjin\Platform\Models\TokenAccount;
use Enjin\Platform\Models\Wallet;
use Enjin\Platform\Rules\Traits\HasDataAwareRule;
use Enjin\Platform\Support\Account;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Arr;
Expand Down Expand Up @@ -51,97 +49,78 @@ public function __construct(protected ?string $collectionId)
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
/**
* The total circulating supply of tokens must not exceed the collection's maximum token supply.
* For example, if the maximum token count is 10 and the maximum token supply is 10,
* the total circulating supply must not exceed 100.
*/
if ($this->collectionId
&& ($collection = Collection::firstWhere(['collection_chain_id' => $this->collectionId]))
&& !is_null($this->limit = $collection->max_token_supply)
) {
if (Arr::get($this->data, str_replace('tokenQuantityPerClaim', 'type', $attribute)) == BeamType::MINT_ON_DEMAND->name) {
if (!$collection->max_token_supply >= $value) {
$fail($this->maxTokenSupplyMessage)
->translate([
'limit' => $this->limit,
]);

return;
}
if ((Arr::get($this->data, str_replace('tokenQuantityPerClaim', 'type', $attribute)) == BeamType::MINT_ON_DEMAND->name
&& !$collection->max_token_supply >= $value)
|| $this->limit == 0
) {
$fail($this->maxTokenSupplyMessage)->translate(['limit' => $this->limit]);

return;
}

$tokenIds = Arr::get($this->data, str_replace('tokenQuantityPerClaim', 'tokenIds', $attribute));
$integers = collect($tokenIds)->filter(fn ($val) => false === $this->integerRange($val))->all();
if ($integers) {
$wallet = Wallet::firstWhere(['public_key' => Account::daemonPublicKey()]);
$collection = Collection::firstWhere(['collection_chain_id' => $this->collectionId]);
if (!$wallet || !$collection) {
$fail($this->maxTokenSupplyMessage)
->translate([
'limit' => $this->limit,
]);

return;
}
$accounts = TokenAccount::join('tokens', 'tokens.id', '=', 'token_accounts.token_id')
->where('token_accounts.wallet_id', $wallet->id)
->where('token_accounts.collection_id', $collection->id)
->whereIn('tokens.token_chain_id', $integers)
->selectRaw('tokens.token_chain_id, sum(token_accounts.balance) as balance')
->groupBy('tokens.token_chain_id')
->get();

$claims = BeamClaim::whereHas(
'beam',
fn ($query) => $query->where('collection_chain_id', $this->collectionId)->where('end', '>', now())
)->where('type', BeamType::TRANSFER_TOKEN->name)
->whereIn('token_chain_id', $integers)
->whereNull('wallet_public_key')
->selectRaw('token_chain_id, sum(quantity) as quantity')
->groupBy('token_chain_id')
->pluck('quantity', 'token_chain_id');
foreach ($accounts as $account) {
if ((int) $account->balance < $value + Arr::get($claims, $account->token_chain_id, 0)) {
$fail($this->maxTokenBalanceMessage)->translate();

return;
}
}
if ($collection->max_token_count == 0) {
$fail('enjin-platform-beam::validation.max_token_count')->translate(['limit' => $this->limit]);

return;
}

$ranges = collect($tokenIds)->filter(fn ($val) => false !== $this->integerRange($val))->all();
if ($ranges) {
$wallet = Wallet::firstWhere(['public_key' => Account::daemonPublicKey()]);
$collection = Collection::firstWhere(['collection_chain_id' => $this->collectionId]);
if (!$wallet || !$collection) {
$fail($this->maxTokenSupplyMessage)
->translate([
'limit' => $this->limit,
]);

return;
}
foreach ($ranges as $range) {
[$from, $to] = $this->integerRange($range);
$accounts = TokenAccount::join('tokens', 'tokens.id', '=', 'token_accounts.token_id')
->where('token_accounts.wallet_id', $wallet->id)
->where('token_accounts.collection_id', $collection->id)
->whereBetween('tokens.token_chain_id', [(int) $from, (int) $to])
->selectRaw('tokens.token_chain_id, sum(token_accounts.balance) as balance')
->groupBy('tokens.token_chain_id')
->get();

$claims = BeamClaim::whereHas(
'beam',
fn ($query) => $query->where('collection_chain_id', $this->collectionId)->where('end', '>', now())
)->where('type', BeamType::TRANSFER_TOKEN->name)
->whereBetween('token_chain_id', [(int) $from, (int) $to])
->whereNull('wallet_public_key')
->selectRaw('token_chain_id, sum(quantity) as quantity')
->groupBy('token_chain_id')
->pluck('quantity', 'token_chain_id');
foreach ($accounts as $account) {
if ((int) $account->balance < $value + Arr::get($claims, $account->token_chain_id, 0)) {
$fail($this->maxTokenBalanceMessage)->translate();
$this->limit = $collection->max_token_supply * ($collection->max_token_count ?? 1);

$balanceCount = TokenAccount::where('token_accounts.collection_id', $collection->id)->sum('balance');
$claimCount = BeamClaim::where('type', BeamType::MINT_ON_DEMAND->name)
->whereHas('beam', fn ($query) => $query->where('collection_chain_id', $this->collectionId)->where('end', '>', now()))
->claimable()
->sum('quantity');

$tokenCount = 0;
$tokenCount = collect($this->data['tokens'])
->reduce(function ($carry, $token) {

if (Arr::get($token, 'tokenIds')) {
return collect($token['tokenIds'])->reduce(function ($val, $tokenId) use ($token) {
$range = $this->integerRange($tokenId);
$claimQuantity = Arr::get($token, 'claimQuantity', 1);
$quantityPerClaim = Arr::get($token, 'tokenQuantityPerClaim', 1);

return $val + (
$range === false
? $claimQuantity * $quantityPerClaim
: (($range[1] - $range[0]) + 1) * $claimQuantity * $quantityPerClaim
);
}, $carry);
}

if (Arr::get($token, 'tokenIdDataUpload')) {
$total = 0;
$handle = fopen($token['tokenIdDataUpload']->getPathname(), 'r');
while (($line = fgets($handle)) !== false) {
$range = $this->integerRange(trim($line));
$claimQuantity = Arr::get($token, 'claimQuantity', 1);
$quantityPerClaim = Arr::get($token, 'tokenQuantityPerClaim', 1);
$total += (
$range === false
? $claimQuantity * $quantityPerClaim
: (($range[1] - $range[0]) + 1) * $claimQuantity * $quantityPerClaim
);
}
fclose($handle);

return $total;
}
}

}, $tokenCount);

if ($this->limit < $balanceCount + $claimCount + $tokenCount) {
$fail($this->maxTokenSupplyMessage)->translate(['limit' => $this->limit]);
}
}
}
Expand Down
16 changes: 7 additions & 9 deletions tests/Feature/GraphQL/Mutations/CreateBeamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,18 +344,18 @@ public function test_it_will_fail_with_invalid_claim_quantity(): void
);
$this->assertArraySubset(['tokens.0.claimQuantity' => ['The token count exceeded the maximum limit of 0 for this collection.']], $response['error']);

$this->collection->update(['max_token_count' => 2]);
$response = $this->graphql(
$this->method,
$data = array_merge(
$this->generateBeamData(BeamType::MINT_ON_DEMAND, 1),
['tokens' => [['tokenIds' => ['1'], 'type' => BeamType::MINT_ON_DEMAND->name]]]
)
),
true
);
$this->assertNotEmpty($response);

$response = $this->graphql($this->method, $data, true);
$this->assertArraySubset(['tokens.0.claimQuantity' => ['The token count exceeded the maximum limit of 2 for this collection.']], $response['error']);
$this->assertArraySubset(['tokens.0.claimQuantity' => ['The token count exceeded the maximum limit of 0 for this collection.']], $response['error']);
}

/**
Expand All @@ -371,20 +371,18 @@ public function test_it_will_fail_with_invalid_token_quantity_per_claim(): void
true
);
$this->assertArraySubset(
['tokens.0.tokenQuantityPerClaim' => ['The tokens.0.tokenQuantityPerClaim exceeded the maximum supply limit of 0 for each token for this collection.']],
['tokens.0.tokenQuantityPerClaim' => ['The tokens.0.tokenQuantityPerClaim exceeded the maximum supply limit of 0 for unique tokens for this collection.']],
$response['error']
);

$this->collection->update(['max_token_supply' => 2]);
$response = $this->graphql(
$this->method,
$data = $this->generateBeamData(BeamType::TRANSFER_TOKEN, 1),
$this->generateBeamData(BeamType::TRANSFER_TOKEN, 1),
true
);
$this->assertNotEmpty($response);

$response = $this->graphql($this->method, $data, true);
$this->assertArraySubset(
['tokens.0.tokenQuantityPerClaim' => ['The tokens.0.tokenQuantityPerClaim is invalid, the amount provided is bigger than the token account balance.']],
['tokens.0.tokenQuantityPerClaim' => ['The tokens.0.tokenQuantityPerClaim exceeded the maximum supply limit of 0 for unique tokens for this collection.']],
$response['error']
);
}
Expand Down
Loading

0 comments on commit 148e7cb

Please sign in to comment.