From 9c79d7832ad00580cf536b457bef690ee6ab16a2 Mon Sep 17 00:00:00 2001 From: Michael McLaughlin Date: Fri, 29 Mar 2019 12:22:19 -0500 Subject: [PATCH] added wallet cooldown code --- .travis.yml | 1 + base.yml | 1 + bat-utils/lib/extras-utils.js | 6 ++++++ bat-utils/lib/extras-utils.test.js | 12 ++++++------ ledger/controllers/grants.js | 10 ++++++++-- ledger/lib/grants.js | 15 +++++++++++++++ test/ledger/grants.integration.test.js | 18 ++++++++++++++++++ 7 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 ledger/lib/grants.js diff --git a/.travis.yml b/.travis.yml index ba5021dee..dc58a7f75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ env: - "TESTING_COHORTS=test" - "TOKEN_LIST=foobarfoobar" - "UPHOLD_DONOR_CARD_ID=6654ecb0-6079-4f6c-ba58-791cc890a561" + - "WALLET_COOLDOWN_HRS=0" - "VOTING_COHORTS=control,grant,test,ads,safetynet" - secure: "JQ9WipBrcCPtXFwv/fmzhv8LPz/tx/jCPpeTK0KmSZr4Yqs5kMG0YGH3QeT1YDpiRakW9X5us9tsnsfHuslvZ8Z565lc0wIK7b7CUcEzVuBts6cfVQtOpqKKYjw6jyq5TraTYawYGUfeaCkP+35oM/1fs4vPy6MbH5Plzg6f9iMfB+BFysRT20JYKXcW0LfihHRKStaAJYKDD2mpdY4kStu7vYB66LgR6Solw1IUwgJzfKst/dM6PJspfmNkFaHCoBNMEYcUJmeJPqmLCnfIA+eRCrSMfHOChjzCV6JbmcD11FOydISEhdt67X+5TDArePYVssrj7NygVlrn8TOvk6InXuul6n+oIpS0vz5e5QdxrReLLwb+WJPK7xKWD56Ko2gfTwtEUdevLzQhKfPMs5wW5ujjlcjAqi5PmC87Q+bxCjet19EdJ4j09y2NSKRo1LMzu1WIM6o1mIatha0vJEHa05XAUsuD9lCGrt/aG4DLYh5Q5gJP/SYDoMl+wJku254KJN+WesxaSREhgdqtV13GFnMofI9r/vq4wKrlcbsMh3UD337NZlNVZzONINeJ4afN3TRK+LS8bHGsss/ZpLcnfT0uYMXzneQQhkKmKWkss8VOLEZ2ccLRG0LNWsOJXznBE/SqXaxSK4NkgXQDyfVUuAj7GPyIDX6hygsguyA=" - secure: "be8ASUNkGT1MpO/wfwVQKuo0Absf5xQAnjXRmsrM105n+vGXdq1c/fl13wow7d/PQtR27tNlJ5UmoeROUCBLlOKm+bWWZ7wFThsxOQtHk3yJoaVtIApo5cwEQcMi/92zWQKDwO7ZuK+xY9oLiYng2yayfuxBUEzWlNxS0cMsmq6Xnf1qG8USVCqvo8gQp/X8hliscFWRbCJ+lzggmlIsoTe6RElF9N3yXngvWRJY/GDjOe3FD2wJNDoE8wYwHcxKvfcdL0c8dWhHl5KC366ZSiBMJHrwv2aVjUgwQ2Azsdehxy1bsqyiH2bS/ifH8FUosjMA/g8qRB89EgaLCS5NndIVl1q0miuRv20kMaF6syTOH3HyDi/o2L9tg1Z/XSuNSVI2o+A8yJfKiY1AwjCR8aQ+uyKj9kOg7nTlBElHrOcAOaiC0Rg13TZ2230IbFanQFIqDL0B8xQx1EAxsArgWIrebqSd1QbVYKEHE5+2dWXExPX+7e217rFvEz5Ltxq1U9IdHHnmcTxUrylr800ub2PtOyZvRNbBFXq5tDpENLl/lV8yf0Qm2q+8jUvqXZIG0akIqC5b0cpHEtxJcAGvfqFx8vZUV1WW4TVbxApmndSGFw2bF6psx6SJUDrdFfl87u1Rb7wWtesDXAMrfIphlr0AcH2P7XDqxAjifuQgtdk=" diff --git a/base.yml b/base.yml index f5a0d8c95..8267b9b86 100644 --- a/base.yml +++ b/base.yml @@ -70,4 +70,5 @@ services: - UPHOLD_DONOR_CARD_ID - UPHOLD_ENVIRONMENT - VOTING_COHORTS + - WALLET_COOLDOWN_HRS - YOUTUBE_API_KEY diff --git a/bat-utils/lib/extras-utils.js b/bat-utils/lib/extras-utils.js index 097cf3d21..9529fdc66 100644 --- a/bat-utils/lib/extras-utils.js +++ b/bat-utils/lib/extras-utils.js @@ -20,6 +20,7 @@ module.exports = { isYoutubeChannelId, normalizeChannel, justDate, + hexTime, BigNumber } @@ -48,6 +49,11 @@ function documentOlderThan (olderThanDays, anchorTime, _id) { return createdTimestamp(_id) < (anchorTime - (DAY_MS * olderThanDays)) } +function hexTime (now = new Date()) { + const offsetSeconds = Math.floor(now / 1000) + return offsetSeconds.toString(16) + '0000000000000000' +} + function createdTimestamp (id) { return new Date(parseInt(id.toHexString().substring(0, 8), 16) * 1000).getTime() } diff --git a/bat-utils/lib/extras-utils.test.js b/bat-utils/lib/extras-utils.test.js index 09c7e3914..c7c9d7b49 100644 --- a/bat-utils/lib/extras-utils.test.js +++ b/bat-utils/lib/extras-utils.test.js @@ -4,6 +4,7 @@ import { ObjectID } from 'mongodb' import { + hexTime, surveyorChoices, createdTimestamp, timeout, @@ -18,13 +19,11 @@ dotenv.config() const objectId = ObjectID('5b11685dd28b11258d50c1f4') const objectDate = (new Date('2018-06-01T15:38:05.000Z')).getTime() test('createdTimestamp', (t) => { - t.plan(1) const fromId = createdTimestamp(objectId) t.is(fromId, objectDate) }) test('timeout', (t) => { - t.plan(1) let bool = false timeout(495).then(() => { bool = true @@ -42,7 +41,6 @@ test('timeout', (t) => { }) test('documentOlderThan', (t) => { - t.plan(3) t.true(documentOlderThan(-1, objectDate, objectId)) t.false(documentOlderThan(1, objectDate, objectId)) // lt not lte @@ -50,14 +48,12 @@ test('documentOlderThan', (t) => { }) test('isYoutubeChannelId', (t) => { - t.plan(3) t.true(isYoutubeChannelId('UCFNTTISby1c_H-rm5Ww5rZg')) t.false(isYoutubeChannelId('UCFNTTISby1c_H-rm5Ww5rZ')) t.false(isYoutubeChannelId('Brave')) }) test('normalizeChannel', (t) => { - t.plan(4) t.is(normalizeChannel('youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg'), 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg') t.is(normalizeChannel('youtube#channel:Brave'), 'youtube#user:Brave') t.is(normalizeChannel('twitch#channel:Brave'), 'twitch#author:Brave') @@ -65,9 +61,13 @@ test('normalizeChannel', (t) => { }) test('surveyorChoices', (t) => { - t.plan(4) t.deepEqual(surveyorChoices(0.55), [6, 10, 14, 20, 40], 'increment is less than') t.deepEqual(surveyorChoices(0.5), [6, 10, 14, 20, 40], 'increment is equal to') t.deepEqual(surveyorChoices(2), [3, 5, 7, 10, 20], 'increment can be above range') t.deepEqual(surveyorChoices(0.02), [30, 50, 70, 100], 'increment can be below range') }) + +test('hexTime', (t) => { + t.is(hexTime(new Date('2019-01-01')), '5c2aad800000000000000000', 'returns hexidecimal version of date') + t.is(hexTime(new Date('2018-01-01')), '5a497a000000000000000000', 'returns hexidecimal version of any date') +}) diff --git a/ledger/controllers/grants.js b/ledger/controllers/grants.js index 26a4ff0ac..3e1547a8b 100644 --- a/ledger/controllers/grants.js +++ b/ledger/controllers/grants.js @@ -6,7 +6,9 @@ const bson = require('bson') const underscore = require('underscore') const uuidV4 = require('uuid/v4') const wreck = require('wreck') - +const { + cooldownOffset +} = require('../lib/grants') const utils = require('bat-utils') const braveJoi = utils.extras.joi const braveHapi = utils.extras.hapi @@ -265,6 +267,7 @@ const getGrant = (protocolVersion) => (runtime) => { const promotions = runtime.database.get('promotions', debug) const wallets = runtime.database.get('wallets', debug) let entries, promotionIds, wallet + let walletTooYoung = false if (qaOnlyP(request)) return reply(boom.notFound()) @@ -276,6 +279,9 @@ const getGrant = (protocolVersion) => (runtime) => { wallet.grants.forEach((grant) => { promotionIds.push(grant.promotionId) }) } underscore.extend(query, { promotionId: { $nin: promotionIds } }) + const offset = cooldownOffset() + walletTooYoung = braveUtils.createdTimestamp(wallet._id) > (new Date() - offset) + debug('offset', { offset, walletTooYoung }) } if (protocolVersion === 4 && !paymentId) { @@ -299,7 +305,7 @@ const getGrant = (protocolVersion) => (runtime) => { underscore.extend(query, { providerId: wallet.addresses.CARD_ID }) } const counted = await grants.count(query) - if (counted !== 0) { + if (counted !== 0 && !walletTooYoung) { filteredPromotions.push({ promotionId, type }) } } diff --git a/ledger/lib/grants.js b/ledger/lib/grants.js new file mode 100644 index 000000000..11229034b --- /dev/null +++ b/ledger/lib/grants.js @@ -0,0 +1,15 @@ +const _ = require('underscore') +const { WALLET_COOLDOWN_HRS } = process.env +module.exports = { + defaultCooldownHrs, + cooldownOffset +} + +function defaultCooldownHrs (hours) { + const hrs = _.isUndefined(hours) ? WALLET_COOLDOWN_HRS : hours + return hrs ? (+hrs || 0) : 24 +} + +function cooldownOffset (hours = defaultCooldownHrs()) { + return hours * 60 * 60 * 1000 +} diff --git a/test/ledger/grants.integration.test.js b/test/ledger/grants.integration.test.js index 0759a565c..ce7a876b1 100644 --- a/test/ledger/grants.integration.test.js +++ b/test/ledger/grants.integration.test.js @@ -18,6 +18,10 @@ import { timeout, uint8tohex } from 'bat-utils/lib/extras-utils' +import { + defaultCooldownHrs, + cooldownOffset +} from '../../ledger/lib/grants' test.before(cleanDbs) test.after(cleanDbs) @@ -28,6 +32,20 @@ const expired = {'grants': ['eyJhbGciOiJFZERTQSIsImtpZCI6IiJ9.eyJhbHRjdXJyZW5jeS const BAT_CAPTCHA_BRAVE_TOKEN = 'eyJhbGciOiJSUzI1NiIsIng1YyI6WyJNSUlGa2pDQ0JIcWdBd0lCQWdJUVJYcm9OMFpPZFJrQkFBQUFBQVB1bnpBTkJna3Foa2lHOXcwQkFRc0ZBREJDTVFzd0NRWURWUVFHRXdKVlV6RWVNQndHQTFVRUNoTVZSMjl2WjJ4bElGUnlkWE4wSUZObGNuWnBZMlZ6TVJNd0VRWURWUVFERXdwSFZGTWdRMEVnTVU4eE1CNFhEVEU0TVRBeE1EQTNNVGswTlZvWERURTVNVEF3T1RBM01UazBOVm93YkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ1RDa05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNURFUxdmRXNTBZV2x1SUZacFpYY3hFekFSQmdOVkJBb1RDa2R2YjJkc1pTQk1URU14R3pBWkJnTlZCQU1URW1GMGRHVnpkQzVoYm1SeWIybGtMbU52YlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTmpYa3owZUsxU0U0bSsvRzV3T28rWEdTRUNycWRuODhzQ3BSN2ZzMTRmSzBSaDNaQ1laTEZIcUJrNkFtWlZ3Mks5RkcwTzlyUlBlUURJVlJ5RTMwUXVuUzl1Z0hDNGVnOW92dk9tK1FkWjJwOTNYaHp1blFFaFVXWEN4QURJRUdKSzNTMmFBZnplOTlQTFMyOWhMY1F1WVhIRGFDN09acU5ub3NpT0dpZnM4djFqaTZIL3hobHRDWmUybEorN0d1dHpleEtweHZwRS90WlNmYlk5MDVxU2xCaDlmcGowMTVjam5RRmtVc0FVd21LVkFVdWVVejR0S2NGSzRwZXZOTGF4RUFsK09raWxNdElZRGFjRDVuZWw0eEppeXM0MTNoYWdxVzBXaGg1RlAzOWhHazlFL0J3UVRqYXpTeEdkdlgwbTZ4RlloaC8yVk15WmpUNEt6UEpFQ0F3RUFBYU9DQWxnd2dnSlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBVEFNQmdOVkhSTUJBZjhFQWpBQU1CMEdBMVVkRGdRV0JCUXFCUXdHV29KQmExb1RLcXVwbzRXNnhUNmoyREFmQmdOVkhTTUVHREFXZ0JTWTBmaHVFT3ZQbSt4Z254aVFHNkRyZlFuOUt6QmtCZ2dyQmdFRkJRY0JBUVJZTUZZd0p3WUlLd1lCQlFVSE1BR0dHMmgwZEhBNkx5OXZZM053TG5CcmFTNW5iMjluTDJkMGN6RnZNVEFyQmdnckJnRUZCUWN3QW9ZZmFIUjBjRG92TDNCcmFTNW5iMjluTDJkemNqSXZSMVJUTVU4eExtTnlkREFkQmdOVkhSRUVGakFVZ2hKaGRIUmxjM1F1WVc1a2NtOXBaQzVqYjIwd0lRWURWUjBnQkJvd0dEQUlCZ1puZ1F3QkFnSXdEQVlLS3dZQkJBSFdlUUlGQXpBdkJnTlZIUjhFS0RBbU1DU2dJcUFnaGg1b2RIUndPaTh2WTNKc0xuQnJhUzVuYjI5bkwwZFVVekZQTVM1amNtd3dnZ0VFQmdvckJnRUVBZFo1QWdRQ0JJSDFCSUh5QVBBQWR3Q2t1UW1RdEJoWUZJZTdFNkxNWjNBS1BEV1lCUGtiMzdqamQ4ME95QTNjRUFBQUFXWmREM1BMQUFBRUF3QklNRVlDSVFDU1pDV2VMSnZzaVZXNkNnK2dqLzl3WVRKUnp1NEhpcWU0ZVk0Yy9teXpqZ0loQUxTYmkvVGh6Y3pxdGlqM2RrM3ZiTGNJVzNMbDJCMG83NUdRZGhNaWdiQmdBSFVBVmhRR21pL1h3dXpUOWVHOVJMSSt4MFoydWJ5WkVWekE3NVNZVmRhSjBOMEFBQUZtWFE5ejVBQUFCQU1BUmpCRUFpQmNDd0E5ajdOVEdYUDI3OHo0aHIvdUNIaUFGTHlvQ3EySzAreUxSd0pVYmdJZ2Y4Z0hqdnB3Mm1CMUVTanEyT2YzQTBBRUF3Q2tuQ2FFS0ZVeVo3Zi9RdEl3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUk5blRmUktJV2d0bFdsM3dCTDU1RVRWNmthenNwaFcxeUFjNUR1bTZYTzQxa1p6d0o2MXdKbWRSUlQvVXNDSXkxS0V0MmMwRWpnbG5KQ0YyZWF3Y0VXbExRWTJYUEx5RmprV1FOYlNoQjFpNFcyTlJHelBodDNtMWI0OWhic3R1WE02dFg1Q3lFSG5UaDhCb200L1dsRmloemhnbjgxRGxkb2d6L0syVXdNNlM2Q0IvU0V4a2lWZnYremJKMHJqdmc5NEFsZGpVZlV3a0k5Vk5NakVQNWU4eWRCM29MbDZnbHBDZUY1ZGdmU1g0VTl4MzVvai9JSWQzVUUvZFBwYi9xZ0d2c2tmZGV6dG1VdGUvS1Ntcml3Y2dVV1dlWGZUYkkzenNpa3daYmtwbVJZS21qUG1odjRybGl6R0NHdDhQbjhwcThNMktEZi9QM2tWb3QzZTE4UT0iLCJNSUlFU2pDQ0F6S2dBd0lCQWdJTkFlTzBtcUdOaXFtQkpXbFF1REFOQmdrcWhraUc5dzBCQVFzRkFEQk1NU0F3SGdZRFZRUUxFeGRIYkc5aVlXeFRhV2R1SUZKdmIzUWdRMEVnTFNCU01qRVRNQkVHQTFVRUNoTUtSMnh2WW1Gc1UybG5iakVUTUJFR0ExVUVBeE1LUjJ4dlltRnNVMmxuYmpBZUZ3MHhOekEyTVRVd01EQXdOREphRncweU1URXlNVFV3TURBd05ESmFNRUl4Q3pBSkJnTlZCQVlUQWxWVE1SNHdIQVlEVlFRS0V4VkhiMjluYkdVZ1ZISjFjM1FnVTJWeWRtbGpaWE14RXpBUkJnTlZCQU1UQ2tkVVV5QkRRU0F4VHpFd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURRR005RjFJdk4wNXprUU85K3ROMXBJUnZKenp5T1RIVzVEekVaaEQyZVBDbnZVQTBRazI4RmdJQ2ZLcUM5RWtzQzRUMmZXQllrL2pDZkMzUjNWWk1kUy9kTjRaS0NFUFpSckF6RHNpS1VEelJybUJCSjV3dWRnem5kSU1ZY0xlL1JHR0ZsNXlPRElLZ2pFdi9TSkgvVUwrZEVhbHROMTFCbXNLK2VRbU1GKytBY3hHTmhyNTlxTS85aWw3MUkyZE44RkdmY2Rkd3VhZWo0YlhocDBMY1FCYmp4TWNJN0pQMGFNM1Q0SStEc2F4bUtGc2JqemFUTkM5dXpwRmxnT0lnN3JSMjV4b3luVXh2OHZObWtxN3pkUEdIWGt4V1k3b0c5aitKa1J5QkFCazdYckpmb3VjQlpFcUZKSlNQazdYQTBMS1cwWTN6NW96MkQwYzF0Skt3SEFnTUJBQUdqZ2dFek1JSUJMekFPQmdOVkhROEJBZjhFQkFNQ0FZWXdIUVlEVlIwbEJCWXdGQVlJS3dZQkJRVUhBd0VHQ0NzR0FRVUZCd01DTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0hRWURWUjBPQkJZRUZKalIrRzRRNjgrYjdHQ2ZHSkFib090OUNmMHJNQjhHQTFVZEl3UVlNQmFBRkp2aUIxZG5IQjdBYWdiZVdiU2FMZC9jR1lZdU1EVUdDQ3NHQVFVRkJ3RUJCQ2t3SnpBbEJnZ3JCZ0VGQlFjd0FZWVphSFIwY0RvdkwyOWpjM0F1Y0d0cExtZHZiMmN2WjNOeU1qQXlCZ05WSFI4RUt6QXBNQ2VnSmFBamhpRm9kSFJ3T2k4dlkzSnNMbkJyYVM1bmIyOW5MMmR6Y2pJdlozTnlNaTVqY213d1B3WURWUjBnQkRnd05qQTBCZ1puZ1F3QkFnSXdLakFvQmdnckJnRUZCUWNDQVJZY2FIUjBjSE02THk5d2Eya3VaMjl2Wnk5eVpYQnZjMmwwYjNKNUx6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFHb0ErTm5uNzh5NnBSamQ5WGxRV05hN0hUZ2laL3IzUk5Ha21VbVlIUFFxNlNjdGk5UEVhanZ3UlQyaVdUSFFyMDJmZXNxT3FCWTJFVFV3Z1pRK2xsdG9ORnZoc085dHZCQ09JYXpwc3dXQzlhSjl4anU0dFdEUUg4TlZVNllaWi9YdGVEU0dVOVl6SnFQalk4cTNNRHhyem1xZXBCQ2Y1bzhtdy93SjRhMkc2eHpVcjZGYjZUOE1jRE8yMlBMUkw2dTNNNFR6czNBMk0xajZieWtKWWk4d1dJUmRBdktMV1p1L2F4QlZielltcW13a201ekxTRFc1bklBSmJFTENRQ1p3TUg1NnQyRHZxb2Z4czZCQmNDRklaVVNweHU2eDZ0ZDBWN1N2SkNDb3NpclNtSWF0ai85ZFNTVkRRaWJldDhxLzdVSzR2NFpVTjgwYXRuWnoxeWc9PSJdfQ.eyJub25jZSI6InhnTUdCWVl3aW53dVhrQ2x4OXNYVDdtSWxLMEtodDJaWW5KaGRtVmZibTl1WTJVeE5UUXdORGszTnpJME1ESTIiLCJ0aW1lc3RhbXBNcyI6MTU0MDQ5NzcyNTQ4MCwiYXBrUGFja2FnZU5hbWUiOiJjb20uYnJhdmUuYnJvd3NlciIsImFwa0RpZ2VzdFNoYTI1NiI6IldPRTVQRk9pNjI4UjhRZHAxa3B2UUhMVVNwWUtnWjU2YUZkbFJyTE5UZ3M9IiwiY3RzUHJvZmlsZU1hdGNoIjp0cnVlLCJhcGtDZXJ0aWZpY2F0ZURpZ2VzdFNoYTI1NiI6WyJNcUw4ZE5jeEVGaFo1YWhkOFcyVjhRTFlXeUlKbTRCa3hkaVJYR0hhMGVBPSJdLCJiYXNpY0ludGVncml0eSI6dHJ1ZX0.K39pNLtS7w-jlnNlP1fz31RGH-xP23t_FyInL3FrxNJGQq5oRMpkBGUeE49sOeUJMi8gjYpQR1Ek2-3M8gS_0IwOUFcIXJjAJuLVJHwg_i0hxtgJLRvCAS3ifsfk-UX7HsYKdY1voUsPtZ9ilYLIJCY5Gy5uHqedgznDKyrGYKPMuiMNyfkp88AjN2XA9D2Axqys9s67uEivMF37HUDK_Kqh8uYF276vs9SU7ovedbz7tG1suGUT9zpEDCZBLqdhj2wfdiXphhOZJG2ogMUzp3IOCRLYsfWSha-OQX3UI84cYUG1Ubrqd4mahl5dpkxclkMr-zyFQC5WNCQLO2OA_g' +test('default cooldown hrs', async (t) => { + t.is(defaultCooldownHrs(), defaultCooldownHrs(process.env.WALLET_COOLDOWN_HRS), 'uses env var for default') + t.is(defaultCooldownHrs(12), 12, 'can be passed number') + t.is(defaultCooldownHrs('12'), 12, 'can be passed numeric string') + t.is(defaultCooldownHrs('0'), 0, 'can be passed falsey numeric string') + t.is(defaultCooldownHrs('a'), 0, 'defaults to 0 if passed non numeric values') +}) + +test('cooldown offset', async (t) => { + t.is(cooldownOffset(), cooldownOffset(defaultCooldownHrs()), 'calculates hours to offset in terms of milliseconds') + t.is(cooldownOffset(12), 12 * 60 * 60 * 1000, 'gives back in ms') + t.is(cooldownOffset({}), NaN, 'only takes numeric values') +}) + test('grants: add expired grant and make sure it does not add to wallet', async t => { t.plan(10) let body, item