Allow users to force-update their linked ETH address

Removed sc_identity_id from Player table because it enforces ETH addresses to be used as the single source of truth for users and prevents conflicts when users update their ETH address in the ledger. sc_identity_id is not needing anywhere in our backend / data model.
This commit is contained in:
Hammad Jutt
2021-06-09 13:01:04 -06:00
committed by Alec LaLonde
parent ba0be9fb09
commit 63eeca916f
13 changed files with 61 additions and 104 deletions

View File

@@ -83,6 +83,12 @@ yarn backend:dev
yarn web:dev
```
### Run Discord Bot
```shell script
yarn discord-bot dev
```
### Tooling
Start Hasura console

View File

@@ -310,9 +310,8 @@
- player_type_id
- rank
- role
- sc_identity_id
- total_xp
- timezone
- total_xp
- username
filter: {}
allow_aggregations: true

View File

@@ -0,0 +1,3 @@
ALTER TABLE "public"."player" ADD COLUMN "sc_identity_id" text;
ALTER TABLE "public"."player" ALTER COLUMN "sc_identity_id" DROP NOT NULL;
ALTER TABLE "public"."player" ADD CONSTRAINT Player_scIdentityId_key UNIQUE (sc_identity_id);

View File

@@ -0,0 +1 @@
ALTER TABLE "public"."player" DROP COLUMN "sc_identity_id" CASCADE;

View File

@@ -1,4 +1,8 @@
import { Constants, isNotNullOrUndefined } from '@metafam/utils';
import {
Constants,
getLatestEthAddress,
isNotNullOrUndefined,
} from '@metafam/utils';
import bluebird from 'bluebird';
import { Request, Response } from 'express';
import fetch from 'node-fetch';
@@ -7,9 +11,6 @@ import { SCAccountsData, SCAlias, sourcecred as sc } from 'sourcecred';
import {
AccountType_Enum,
Player_Account_Constraint,
Player_Constraint,
Player_Insert_Input,
Player_Update_Column,
} from '../../../lib/autogen/hasura-sdk';
import { client } from '../../../lib/hasuraClient';
import { computeRank } from '../../../lib/rankHelpers';
@@ -73,10 +74,7 @@ export const migrateSourceCredAccounts = async (
const discordId = linkedAccounts.find(({ type }) => type === 'DISCORD')
?.identifier;
const ethAddress = a.account.identity.aliases.find((alias) => {
const parts = sc.core.graph.NodeAddress.toParts(alias.address);
return parts.indexOf('ethereum') > 0;
})?.description;
const ethAddress = getLatestEthAddress(a.account.identity);
if (!ethAddress) return null;
@@ -148,32 +146,11 @@ export const migrateSourceCredAccounts = async (
},
{ concurrency: 10 },
);
const usersToInsert: Player_Insert_Input[] = result
.filter(isNotNullOrUndefined)
.map((player) => ({
username: player.ethereum_address,
ethereum_address: player.ethereum_address,
sc_identity_id: player.scIdentityId,
rank: player.rank,
total_xp: player.totalXp,
}));
const usersSkipped = result.filter(isNotNullOrUndefined);
const resultInsert = await client.UpsertPlayer({
objects: usersToInsert,
onConflict: {
constraint: Player_Constraint.PlayerEthereumAddressUniqueKey,
update_columns: [
Player_Update_Column.ScIdentityId,
Player_Update_Column.Username,
Player_Update_Column.TotalXp,
Player_Update_Column.Rank,
],
},
});
res.json({
resultInsert,
numUpdated: accountList.length - usersToInsert.length,
numInserted: usersToInsert.length,
numSkipped: usersSkipped.length,
numUpdated: accountList.length - usersSkipped.length,
});
} catch (e) {
console.warn('Error migrating players/accounts', e.message);

View File

@@ -29,60 +29,17 @@ export const UpsertAccount = gql`
}
`;
export const UpsertPlayer = gql`
mutation UpsertPlayer(
$objects: [player_insert_input!]!
$onConflict: player_on_conflict
) {
insert_player(on_conflict: $onConflict, objects: $objects) {
affected_rows
}
}
`;
export const DeleteDuplicatePlayers = gql`
mutation DeleteDuplicatePlayers($scIds: [String!] = "") {
delete_player_account(
where: { Player: { sc_identity_id: { _in: $scIds } } }
) {
affected_rows
}
delete_player(where: { sc_identity_id: { _in: $scIds } }) {
affected_rows
}
}
`;
export const UpdatePlayer = gql`
mutation UpdatePlayer(
$ethAddress: String
$identityId: String
$username: String
$rank: PlayerRank_enum
$totalXp: numeric
$discordId: String
) {
update_player(
where: {
_or: [
{
ethereum_address: { _eq: $ethAddress }
sc_identity_id: { _eq: $identityId }
}
{
ethereum_address: { _eq: $ethAddress }
sc_identity_id: { _is_null: true }
}
{
ethereum_address: { _eq: $ethAddress }
username: { _eq: $username }
}
{ sc_identity_id: { _eq: $identityId } }
{ discord_id: { _eq: $discordId } }
]
}
where: { ethereum_address: { _eq: $ethAddress } }
_set: {
ethereum_address: $ethAddress
sc_identity_id: $identityId
rank: $rank
total_xp: $totalXp
discord_id: $discordId
@@ -92,7 +49,6 @@ export const UpdatePlayer = gql`
returning {
id
ethereum_address
sc_identity_id
username
}
}

View File

@@ -0,0 +1,4 @@
DISCORD_BOT_TOKEN=
DISCORD_BOT_CLIENT_ID=
DISCORD_BOT_CLIENT_SECRET=
GITHUB_API_TOKEN=

View File

@@ -21,7 +21,7 @@
"dependencies": {
"@types/express": "4.17.11",
"@types/node-fetch": "2.5.10",
"@metafam/utils": "^1.0.0",
"@metafam/utils": "1.0.0",
"@typeit/discord": "4.0.10",
"discord.js": "12.5.3",
"dotenv": "9.0.2",

View File

@@ -5,9 +5,14 @@ import { loadSourceCredLedger, manager } from '../../sourcecred';
const addressUtils = sc.plugins.ethereum.utils.address;
type SetEthAddressArgs = {
ethAddress: string;
force: string;
};
export abstract class SetEthAddress {
@Command('!setAddress :ethAddress')
async setAddress(message: CommandMessage) {
@Command('!setAddress :ethAddress :force')
async setAddress(message: CommandMessage<SetEthAddressArgs>) {
const res = await loadSourceCredLedger();
if (res.error) {
@@ -47,14 +52,21 @@ export abstract class SetEthAddress {
const account = manager.ledger.account(baseIdentityId);
const existing = account.identity.aliases.find((alias) => {
const existingEthAliases = account.identity.aliases.filter((alias) => {
const parts = sc.core.graph.NodeAddress.toParts(alias.address);
return parts.indexOf('ethereum') > 0;
});
if (existing) {
const latestEthAlias = existingEthAliases[existingEthAliases.length - 1];
const shouldForceUpdate = message.args.force === 'force';
if (latestEthAlias && !shouldForceUpdate) {
await message.reply(
`You already have linked the following ETH Address: \`${existing.description}\`.`,
`You already have linked the following ETH Address: \`${latestEthAlias.description}\`. Are you sure you want to update it? Warning: This cannot be undone and you will have to recreate your MyMeta profile!
To force update your address, type \`!setAddress ${ethAddress} force\`.
`,
);
return;
}

View File

@@ -17,6 +17,7 @@
"bignumber.js": "9.0.1",
"ethers": "5.3.0",
"js-base64": "3.6.1",
"uuid": "8.3.2"
"uuid": "8.3.2",
"sourcecred": "0.9.0"
}
}

View File

@@ -5,3 +5,4 @@ export * as DiscordUtil from './discordHelpers';
export * as numbers from './numbers';
export * from './promiseHelpers';
export * from './rankHelpers';
export * from './sourceCredHelpers';

View File

@@ -0,0 +1,15 @@
import { ethers } from 'ethers';
import { SCIdentity, sourcecred as sc } from 'sourcecred';
export const getLatestEthAddress = (identity: SCIdentity): string | null => {
const ethAddress = identity.aliases.find((alias) => {
const parts = sc.core.graph.NodeAddress.toParts(alias.address);
return parts.indexOf('ethereum') > 0;
})?.description;
if (ethAddress && ethers.utils.isAddress(ethAddress)) {
return ethAddress.toLowerCase();
}
return null;
};

View File

@@ -3567,7 +3567,6 @@ type player {
): quest_aggregate!
rank: PlayerRank_enum
role: String
sc_identity_id: String
timezone: String
"""Remote relationship field"""
@@ -3847,7 +3846,6 @@ input player_bool_exp {
quests: quest_bool_exp
rank: PlayerRank_enum_comparison_exp
role: String_comparison_exp
sc_identity_id: String_comparison_exp
timezone: String_comparison_exp
total_xp: numeric_comparison_exp
updated_at: timestamptz_comparison_exp
@@ -3864,9 +3862,6 @@ enum player_constraint {
"""unique or primary key constraint"""
Player_pkey
"""unique or primary key constraint"""
Player_scIdentityId_key
"""unique or primary key constraint"""
Player_username_unique_key
@@ -3904,7 +3899,6 @@ input player_insert_input {
quests: quest_arr_rel_insert_input
rank: PlayerRank_enum
role: String
sc_identity_id: String
timezone: String
total_xp: numeric
updated_at: timestamptz
@@ -3921,7 +3915,6 @@ type player_max_fields {
id: uuid
player_type_id: Int
role: String
sc_identity_id: String
timezone: String
total_xp: numeric
updated_at: timestamptz
@@ -3940,7 +3933,6 @@ input player_max_order_by {
id: order_by
player_type_id: order_by
role: order_by
sc_identity_id: order_by
timezone: order_by
total_xp: order_by
updated_at: order_by
@@ -3957,7 +3949,6 @@ type player_min_fields {
id: uuid
player_type_id: Int
role: String
sc_identity_id: String
timezone: String
total_xp: numeric
updated_at: timestamptz
@@ -3976,7 +3967,6 @@ input player_min_order_by {
id: order_by
player_type_id: order_by
role: order_by
sc_identity_id: order_by
timezone: order_by
total_xp: order_by
updated_at: order_by
@@ -4031,7 +4021,6 @@ input player_order_by {
quests_aggregate: quest_aggregate_order_by
rank: order_by
role: order_by
sc_identity_id: order_by
timezone: order_by
total_xp: order_by
updated_at: order_by
@@ -4076,9 +4065,6 @@ enum player_select_column {
"""column name"""
role
"""column name"""
sc_identity_id
"""column name"""
timezone
@@ -4105,7 +4091,6 @@ input player_set_input {
player_type_id: Int
rank: PlayerRank_enum
role: String
sc_identity_id: String
timezone: String
total_xp: numeric
updated_at: timestamptz
@@ -4713,9 +4698,6 @@ enum player_update_column {
"""column name"""
role
"""column name"""
sc_identity_id
"""column name"""
timezone