[PLAY STEEM x WebApp] Steem Account 모델 클래스

in SCT.암호화폐.Crypto3 years ago (edited)

PLAY STEEM 개발자 이타인클럽입니다.

야심찬 웹앱 개발이 순조롭습니다.

계정 정보 Parsing

플러터를 이용해서 개발하는데, 스팀의 계정 정보를 얻어와서 Dart 모델로 파싱을 해야 합니다. 우리가 잘 모르지만, 스팀 account에는 어마어마한 정보가 있습니다. 그것들을 모두 파싱해줘야 합니다.

플러터 기반 프로젝트 하시는 분들께 참고가 되면 좋겠네요.

참고로 개발은 Flutter 2.0 기준입니다. 2.0 넘어가면서 대부분의 패키지가 null safety를 요구해서, null 처리때문에, ?가 많이 사용되었습니다.

Account 정보 가져오기

account 정보를 얻어오기 위해서는 다음과 같이 api 호출을 해야 합니다.

//database_api.get_accounts
id: 2
jsonrpc: "2.0"
method: "call"
params: ["database_api", "get_accounts", [["etainclub"]]]
0: "database_api"
1: "get_accounts"
2: [["etainclub"]]

실제 호출 하는 코드는 http 패키지가 아니라 Dio라는 패키지를 사용했습니다.

var response = await Dio().post(BlockchainConst.baseURL, data: body);

그러면 http 패키지와 달리 다음과 같은 Dart map 타입 데이터를 얻을 수 있습니다. (아래는 브라우저의 json 데이터를 복사한 것입니다.)

(참고로, account 정보 얻어오는 api가 좀 바뀐거 같네요)

active: {weight_threshold: 1, account_auths: [],…}
balance: "205.054 STEEM"
can_vote: true
comment_count: 0
created: "2017-08-01T13:12:51"
curation_rewards: 1268925
delegated_vesting_shares: "0.000000 VESTS"
downvote_manabar: {current_mana: "24493127104569", last_update_time: 1621401918}
guest_bloggers: []
id: 293226
json_metadata: "{\"profile\":{\"name\":\"etain\",\"about\":\"EtainClub - Help it Forward in our Towns Movement\",\"location\":\"Holographic Universe\",\"website\":\"https://etain.club\",\"profile_image\":\"https://cdn.steemitimages.com/DQmeT9tc2mTVcjnf213RF7uBVXWkFiKBo9Y51KYxLWmBUi8/img_0.9292821390082336.jpg\",\"cover_image\":\"https://scontent-hkg3-1.xx.fbcdn.net/v/t31.0-8/20785732_10212277298200417_4668027306739427338_o.jpg?oh=fe4206333171fee080905c6c433e3adc&oe=5A2FB1FE\"}}"
last_account_recovery: "1970-01-01T00:00:00"
last_account_update: "2021-05-03T15:12:15"
last_owner_update: "2021-05-03T15:12:15"
last_post: "2021-05-19T02:13:36"
last_root_post: "2021-05-19T02:13:36"
last_vote_time: "2021-05-19T05:25:18"
lifetime_vote_count: 0
market_history: []
memo_key: "STM7epMsbSw6rYp4oLZN7kLsxzGneyBDjvqjKpUxZsvBr2pigpTXw"
mined: false
name: "etainclub"
next_vesting_withdrawal: "1969-12-31T23:59:59"
other_history: []
owner: {weight_threshold: 1, account_auths: [],…}
pending_claimed_accounts: 14
post_bandwidth: 0
post_count: 4335
post_history: []
posting: {weight_threshold: 1,…}
posting_json_metadata: "{\"profile\":{\"name\":\"etain\",\"about\":\"EtainClub - Help it Forward in our Towns Movement\",\"location\":\"Holographic Universe\",\"website\":\"https://etain.club\",\"profile_image\":\"https://cdn.steemitimages.com/DQmeT9tc2mTVcjnf213RF7uBVXWkFiKBo9Y51KYxLWmBUi8/img_0.9292821390082336.jpg\",\"cover_image\":\"https://scontent-hkg3-1.xx.fbcdn.net/v/t31.0-8/20785732_10212277298200417_4668027306739427338_o.jpg?oh=fe4206333171fee080905c6c433e3adc&oe=5A2FB1FE\",\"version\":2}}"
posting_rewards: 6501708
proxied_vsf_votes: [0, 0, 0, 0]
proxy: ""
received_vesting_shares: "85050349.714477 VESTS"
recovery_account: "steem"
reputation: "117960183388928"
reset_account: "null"
reward_sbd_balance: "0.834 SBD"
reward_steem_balance: "0.000 STEEM"
reward_vesting_balance: "5972.017441 VESTS"
reward_vesting_steem: "3.178 STEEM"
savings_balance: "0.000 STEEM"
savings_sbd_balance: "0.000 SBD"
savings_sbd_last_interest_payment: "1970-01-01T00:00:00"
savings_sbd_seconds: "0"
savings_sbd_seconds_last_update: "1970-01-01T00:00:00"
savings_withdraw_requests: 0
sbd_balance: "79.598 SBD"
sbd_last_interest_payment: "2021-04-20T12:46:24"
sbd_seconds: "134563373697"
sbd_seconds_last_update: "2021-05-19T05:20:45"
tags_usage: []
to_withdraw: "13555004551232"
transfer_history: []
vesting_balance: "0.000 STEEM"
vesting_shares: "12922158.703801 VESTS"
vesting_withdraw_rate: "0.000000 VESTS"
vote_history: []
voting_manabar: {current_mana: "81670574199595", last_update_time: 1621401918}
voting_power: 8336
withdraw_routes: 0
withdrawn: "13555004551232"
witness_votes: ["ayogom", "clayop", "exnihilo.witness", "italygame", "jayplay.witness", "justyy", "radiokorea",…]
witnesses_voted_for: 17

Account 모델

Account 정보에는 복합 데이터 타입들이 있습니다. 이런 것들을 별도의 클래스로 뺐습니다.

Key auth 클래스

import 'package:json_annotation/json_annotation.dart';

@JsonSerializable()
class KeyAuthority {
  @JsonKey(name: 'weight_threshold')
  int weightThreshold;
  @JsonKey(name: 'account_auths')
  List<dynamic> accountAuths;
  @JsonKey(name: 'key_auths')
  List<dynamic> keyAuths;

  // constructor
  KeyAuthority(
      {required this.weightThreshold,
      required this.accountAuths,
      required this.keyAuths});

  // named constructor
  factory KeyAuthority.fromMap(Map<String, dynamic> map) {
    final authority = KeyAuthority(
        weightThreshold: map['weight_threshold'],
        accountAuths: map['account_auths'],
        keyAuths: map['key_auths']);

    return authority;
  }

  static final empty =
      KeyAuthority(weightThreshold: 0, accountAuths: [], keyAuths: []);
}

json_metadata

json_metadata에는 프로필 데이터가 담겨 있습니다. 이를 위해 별도의 클래스를 만들었습니다.

import 'dart:convert';

import 'package:equatable/equatable.dart';

class ProfileJsonMeta extends Equatable {
  final String? displayName;
  final String? about;
  final String? location;
  final String? website;
  final String? avatarUrl;
  final String? coverUrl;

  ProfileJsonMeta({
    this.displayName,
    this.about,
    this.location,
    this.website,
    this.avatarUrl,
    this.coverUrl,
  });

  factory ProfileJsonMeta.fromJson(String source) =>
      ProfileJsonMeta.fromMap(json.decode(source)['profile']);

  factory ProfileJsonMeta.fromMap(Map<String, dynamic> map) {
    print('ProfileJsonMeta. map: $map');
    return ProfileJsonMeta(
      displayName: map['name'],
      about: map['about'],
      location: map['location'],
      website: map['website'],
      avatarUrl: map['profile_image'],
      coverUrl: map['cover_image'],
    );
  }

  static final empty = ProfileJsonMeta(
    displayName: '',
    about: '',
    location: '',
    website: '',
    avatarUrl: '',
    coverUrl: '',
  );

  @override
  List<Object?> get props =>
      [displayName, about, location, website, avatarUrl, coverUrl];
}

Account 클래스

그럼 이제 Account 모델을 위한 클래스의 필드와 생성자를 살펴보겠습니다.

import 'package:json_annotation/json_annotation.dart';

import 'authority.dart';
import 'profile_json_meta.dart';

@JsonSerializable()
class Account {
  // key authorities
  @JsonKey(name: 'active')
  final KeyAuthority active;
  @JsonKey(name: 'balance')
  final String balance;
  @JsonKey(name: 'can_vote')
  final bool canVote;
  @JsonKey(name: 'comment_count')
  final int commentCount;
  @JsonKey(name: 'created')
  final DateTime createdAt;
  @JsonKey(name: 'curation_rewards')
  final int curationRewards;
  @JsonKey(name: 'delegated_vesting_shares')
  final String delegatedVestingShares;
  @JsonKey(name: 'downvote_manabar')
  final Map<String, dynamic> downvoteMana;
  @JsonKey(name: 'guest_bloggers')
  final List<dynamic>? guestBloggers;
  @JsonKey(name: 'id')
  final int id;
  @JsonKey(name: 'json_metadata')
  final String jsonMeta;
  @JsonKey(name: 'last_account_recovery')
  final DateTime accountRecoveryAt;
  @JsonKey(name: 'last_account_update')
  final DateTime accountUpdateAt;
  @JsonKey(name: 'last_owner_update')
  final DateTime ownerUpdateAt;
  @JsonKey(name: 'last_post')
  final DateTime postAt;
  @JsonKey(name: 'last_root_post')
  final DateTime rootPostAt;
  @JsonKey(name: 'last_vote_time')
  final DateTime voteAt;
  @JsonKey(name: 'lifetime_vote_count')
  final int? lifetimeVoteCount;
  @JsonKey(name: 'market_history')
  final List<dynamic>? marketHistory;
  @JsonKey(name: 'memo_key')
  final String memoKey;
  @JsonKey(name: 'mined')
  final bool mined;
  @JsonKey(name: 'name')
  final String username;
  @JsonKey(name: 'next_vesting_withdrawal')
  final DateTime? nextVestingWithdrawal;
  @JsonKey(name: 'other_history')
  final List<dynamic>? otherHistory;
  @JsonKey(name: 'owner')
  final KeyAuthority owner;
  @JsonKey(name: 'pending_claimed_accounts')
  final int pendingClaimedAccounts;
  @JsonKey(name: 'post_bandwidth')
  final int? postBandwidth;
  @JsonKey(name: 'post_count')
  final int postCount;
  @JsonKey(name: 'post_history')
  final List<dynamic>? postHistory;
  @JsonKey(name: 'posting')
  final KeyAuthority posting;
  @JsonKey(name: 'posting_json_metadata')
  final ProfileJsonMeta profileJsonMeta;
  @JsonKey(name: 'posting_rewards')
  final int postingRewards;
  @JsonKey(name: 'proxied_vsf_votes')
  final List<int>? proxiedVsfVotes;
  @JsonKey(name: 'proxy')
  final String proxy;
  @JsonKey(name: 'received_vesting_shares')
  final String receivedVestingShares;
  @JsonKey(name: 'recovery_account')
  final String recoveryAccount;
  @JsonKey(name: 'reputation')
  final dynamic reputation; // rep is string in get account but int in post..
  @JsonKey(name: 'reset_account')
  final String resetAccount;
  @JsonKey(name: 'reward_sbd_balance')
  final String rewardSbdBalance;
  @JsonKey(name: 'reward_steem_balance')
  final String rewardSteemBalance;
  @JsonKey(name: 'reward_vesting_balance')
  final String rewardVestingBalance; // steem power
  @JsonKey(name: 'reward_vesting_steem')
  final String rewardVestingSteem;
  @JsonKey(name: 'savings_balance')
  final String savingsBalance;
  @JsonKey(name: 'savings_sbd_balance')
  final String? savingsSbdBalance;
  @JsonKey(name: 'savings_sbd_last_interest_payment')
  DateTime? savingsSbdInterestPaymentAt;
  @JsonKey(name: 'savings_sbd_seconds')
  final String? savingsSbdSeconds;
  @JsonKey(name: 'savings_sbd_seconds_last_update')
  final DateTime? savingsSbdSecondsUpdateAt;
  @JsonKey(name: 'savings_withdraw_requests')
  final int savingsWithdrawRequests;
  @JsonKey(name: 'sbd_balance')
  final String sbdBalance;
  @JsonKey(name: 'sbd_last_interest_payment')
  final DateTime? sbdInterestPaymentAt;
  @JsonKey(name: 'sbd_seconds')
  final String sbdSeconds;
  @JsonKey(name: 'sbd_seconds_last_update')
  final DateTime sbdSecondsUpdateAt;
  @JsonKey(name: 'tags_usage')
  final List<dynamic> tagsUsage;
  @JsonKey(name: 'to_withdraw')
  final String toWithdraw;
  @JsonKey(name: 'transfer_history')
  final List<dynamic> transferHistory;
  @JsonKey(name: 'vesting_balance')
  final String vestingBalance;
  @JsonKey(name: 'vesting_shares')
  final String vestingShares;
  @JsonKey(name: 'vesting_withdraw_rate')
  final String vestingWithdrawRate;
  @JsonKey(name: 'vote_history')
  final List<dynamic> voteHistory;
  @JsonKey(name: 'voting_manabar')
  final Map<String, dynamic> votingMana;
  @JsonKey(name: 'voting_power')
  final int votingPower;
  @JsonKey(name: 'withdraw_routes')
  final int? withdrawRoutes;
  @JsonKey(name: 'withdrawn')
  final String? withdrawn;
  @JsonKey(name: 'witness_votes')
  List<String> witnessVotes;
  @JsonKey(name: 'witnesses_voted_for')
  final int witnessesVotedFor;

 // 생성자
Account({
    required this.active,
    required this.balance,
    required this.canVote,
    required this.commentCount,
    required this.createdAt,
    required this.curationRewards,
    required this.delegatedVestingShares,
    required this.downvoteMana,
    this.guestBloggers,
    required this.id,
    required this.jsonMeta,
    required this.accountRecoveryAt,
    required this.accountUpdateAt,
    required this.ownerUpdateAt,
    required this.postAt,
    required this.reputation,
    required this.rootPostAt,
    required this.voteAt,
    this.lifetimeVoteCount,
    this.marketHistory,
    required this.memoKey,
    required this.mined,
    required this.username,
    this.nextVestingWithdrawal,
    this.otherHistory,
    required this.owner,
    required this.pendingClaimedAccounts,
    this.postBandwidth,
    required this.postCount,
    this.postHistory,
    required this.posting,
    required this.profileJsonMeta,
    required this.postingRewards,
    this.proxiedVsfVotes,
    required this.proxy,
    required this.receivedVestingShares,
    required this.recoveryAccount,
    required this.resetAccount,
    required this.rewardSbdBalance,
    required this.rewardSteemBalance,
    required this.rewardVestingBalance,
    required this.rewardVestingSteem,
    required this.savingsBalance,
    this.savingsSbdBalance,
    this.savingsSbdInterestPaymentAt,
    this.savingsSbdSeconds,
    this.savingsSbdSecondsUpdateAt,
    required this.savingsWithdrawRequests,
    required this.sbdBalance,
    this.sbdInterestPaymentAt,
    required this.sbdSeconds,
    required this.sbdSecondsUpdateAt,
    required this.tagsUsage,
    required this.toWithdraw,
    required this.transferHistory,
    required this.vestingBalance,
    required this.vestingShares,
    required this.vestingWithdrawRate,
    required this.voteHistory,
    required this.votingMana,
    required this.votingPower,
    this.withdrawRoutes,
    this.withdrawn,
    required this.witnessVotes,
    required this.witnessesVotedFor,
  });

앞서 Dio 패키지를 쓰면 json이 파싱되어 Dart map 데이터가 얻어진다고 했습니다. 이 데이터를 받아서 클래스 필드값을 채우는 부분을 살펴봅니다.

이 함수는 json_annotation 패키지를 깔면, 자동으로 생성됩니다. 자동으로 생성된 코드의 오류를 잡아주면 됩니다.

factory Account.fromMap(Map<String, dynamic> map) {
    return Account(
      active: KeyAuthority.fromMap(map['active']),
      balance: map['balance'],
      canVote: map['can_vote'],
      commentCount: map['comment_count'],
      createdAt: DateTime.parse(map['created']),
      curationRewards: map['curation_rewards'],
      delegatedVestingShares: map['delegated_vesting_shares'],
      downvoteMana: Map<String, dynamic>.from(map['downvote_manabar']),
      guestBloggers: List<dynamic>.from(map['guest_bloggers']),
      id: map['id'],
      jsonMeta: map['json_metadata'],
      accountRecoveryAt: DateTime.parse(map['last_account_recovery']),
      accountUpdateAt: DateTime.parse(map['last_account_update']),
      ownerUpdateAt: DateTime.parse(map['last_owner_update']),
      postAt: DateTime.parse(map['last_post']),
      rootPostAt: DateTime.parse(map['last_root_post']),
      voteAt: DateTime.parse(map['last_vote_time']),
      lifetimeVoteCount: map['lifetime_vote_count'],
      marketHistory: List<dynamic>.from(map['market_history']),
      memoKey: map['memo_key'],
      mined: map['mined'],
      username: map['name'],
      nextVestingWithdrawal: DateTime.parse(map['next_vesting_withdrawal']),
      otherHistory: List<dynamic>.from(map['other_history']),
      owner: KeyAuthority.fromMap(map['owner']),
      pendingClaimedAccounts: map['pending_claimed_accounts'],
      postBandwidth: map['post_bandwidth'],
      postCount: map['post_count'],
      postHistory: List<dynamic>.from(map['post_history']),
      posting: KeyAuthority.fromMap(map['posting']),
      profileJsonMeta: ProfileJsonMeta.fromJson(map['posting_json_metadata']),
      postingRewards: map['posting_rewards'],
      proxiedVsfVotes: List<int>.from(map['proxied_vsf_votes']),
      proxy: map['proxy'],
      receivedVestingShares: map['received_vesting_shares'],
      recoveryAccount: map['recovery_account'],
      reputation: map['reputation'],
      resetAccount: map['reset_account'],
      rewardSbdBalance: map['reward_sbd_balance'],
      rewardSteemBalance: map['reward_steem_balance'],
      rewardVestingBalance: map['reward_vesting_balance'],
      rewardVestingSteem: map['reward_vesting_steem'],
      savingsBalance: map['savings_balance'],
      savingsSbdBalance: map['savings_sbd_balance'],
      savingsSbdInterestPaymentAt:
          DateTime.parse(map['savings_sbd_last_interest_payment']),
      savingsSbdSeconds: map['savings_sbd_seconds'],
      savingsSbdSecondsUpdateAt:
          DateTime.parse(map['savings_sbd_seconds_last_update']),
      savingsWithdrawRequests: map['savings_withdraw_requests'],
      sbdBalance: map['sbd_balance'],
      sbdInterestPaymentAt: DateTime.parse(map['sbd_last_interest_payment']),
      sbdSeconds: map['sbd_seconds'],
      sbdSecondsUpdateAt: DateTime.parse(map['sbd_seconds_last_update']),
      tagsUsage: List<dynamic>.from(map['tags_usage']),
      toWithdraw: map['to_withdraw'],
      transferHistory: List<dynamic>.from(map['transfer_history']),
      vestingBalance: map['vesting_balance'],
      vestingShares: map['vesting_shares'],
      vestingWithdrawRate: map['vesting_withdraw_rate'],
      voteHistory: List<dynamic>.from(map['vote_history']),
      votingMana: Map<String, dynamic>.from(map['voting_manabar']),
      votingPower: map['voting_power'],
      withdrawRoutes: map['withdraw_routes'],
      withdrawn: map['withdrawn'],
      witnessVotes: List<String>.from(map['witness_votes']),
      witnessesVotedFor: map['witnesses_voted_for'],
    );
  }

Account 초기값

앱을 구현할 때, Account 정보를 얻어오기 전에 Account 객체를 만들어야 할 필요가 있습니다. 이를 위한 초기값을 만들어 둡니다.

static final empty = Account(
    posting: KeyAuthority.empty,
    active: KeyAuthority.empty,
    owner: KeyAuthority.empty,
    balance: '',
    canVote: true,
    createdAt: new DateTime(0),
    curationRewards: 0,
    delegatedVestingShares: '',
    id: 0,
    profileJsonMeta: ProfileJsonMeta.empty,
    postAt: new DateTime(0),
    memoKey: '',
    postCount: 0,
    postingRewards: 0,
    receivedVestingShares: '',
    reputation: 0,
    rewardSbdBalance: '',
    rewardSteemBalance: '',
    rewardVestingBalance: '',
    rewardVestingSteem: '',
    savingsBalance: '',
    savingsSbdBalance: '',
    sbdBalance: '',
    username: '',
    vestingBalance: '',
    vestingShares: '',
    votingMana: {},
    votingPower: 0,
    witnessesVotedFor: 0,
    witnessVotes: [],
    accountRecoveryAt: DateTime(0),
    accountUpdateAt: DateTime(0),
    commentCount: 0,
    downvoteMana: {},
    jsonMeta: '',
    ownerUpdateAt: DateTime(0),
    pendingClaimedAccounts: 0,
    recoveryAccount: '',
    resetAccount: '',
    rootPostAt: DateTime(0),
    sbdSeconds: '',
    sbdSecondsUpdateAt: DateTime(0),
    tagsUsage: [],
    toWithdraw: '',
    transferHistory: [],
    voteAt: DateTime(0),
    voteHistory: [],
    mined: false,
    proxy: '',
    savingsWithdrawRequests: 0,
    vestingWithdrawRate: '0.000000 VESTS',
  );

Account 정보 변환

그럼 모델 클래스가 준비 되었으니깐 steem account 정보를 위에서 만든 Account 클래스 객체로 바꾸는 전체 코드를 살펴봅니다.

코드는 굳이 설명하지 않아도 될 것입니다. 이렇게 하면 steem account 정보가 Account 클래스 객체에 저장됩니다.

Future<Account?> fetchAccount(String username) async {
    final body = {
      'jsonrpc': '2.0',
      'method': BlockchainConst.getAccountsMethod,
      'params': [
        BlockchainConst.getAccountsAPI,
        BlockchainConst.getAccountsParam,
        [
          ['$username']
        ]
      ]
    };
    print('fetchAccounts. body: $body');

    try {
      var response = await Dio().post(BlockchainConst.baseURL, data: body);
      // print('response ${response.data}');

      if (response.statusCode == 200) {
        final userData = response.data['result'][0];
        print('response userData $userData');
        // print('response account name ${userData['name']}');
        final account = Account.fromMap(userData);
        print(
            '[SteemProvider | fetchAccount] account model: ${account.username}');

        return account;
      }
      return null;
    } catch (error) {
      print('failed to fetch user data $error');
      return null;
    }
  }     

Thank you for your support

@steemcurator01
@steemcurator06

Sort:  

코딩 공부 꾸준히 해야하는데 쉽지 않네요 ㅎㅎ
오늘도 좋은 하루 보내세요^^

너무 많은 걸 하시면 탈나요~ 예가 셋에 봉사에 키스팀운영에.. 존경스럽습니다. 개발은 취미 수준이죠.

본업이 개발자이신가요? 그렇던 아니던 플레이스팀 같이 개발해보시겠어요? 개발능력보다 믿음이 가는,통하는 사람과 함께 하면 좋곘습니다.

아니에요 저는 뼛속 문과입니다 ㅎㅎ
빅데이터 관심이 있어서 작년부터 조금씩 공부만 하고 있어요^^;;;

대단하셔요~

무슨 말인지는 모르지만 멀뚱 멀뚱 0_0 다녀갑니다. ㅋㅋ

Coin Marketplace

STEEM 0.19
TRX 0.15
JST 0.029
BTC 63237.60
ETH 2647.23
USDT 1.00
SBD 2.81