Giter VIP home page Giter VIP logo

restake's People

Contributors

albertandrejev avatar autostake-com avatar chillyvee avatar chleeab avatar clemensgg avatar cosmoselon avatar dylanschultzie avatar evmosius avatar gh0stdotexe avatar golden-ratio-staking avatar husonghua avatar iicc1 avatar jasonbonnici avatar jemrickrioux avatar joeabbey avatar joshlopes avatar koltigin avatar lightiv avatar lpx55 avatar mr-kenikh avatar nevzatcirak avatar nullmames avatar poorbuthappy avatar pwnfoo avatar quantumnodeio avatar stan-bl avatar tombeynon avatar windpowerstake avatar zdeadex avatar zenodeapp avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

restake's Issues

Calculate APY

Add a feature to calculate APY and show them in the UI

Handle Ledger devices

I'm learning that when you send a TX you can provide it in two structures; Amino (legacy) and Protobuf. Keplr uses Protobuf if the wallet supports it, otherwise falls back to Amino. Typically transactions are sent in Protobuf format, and the blockchain (or Keplr, CosmJS..?) automatically converts them to Amino if it knows how to do so.

Authz transactions aren't supported for this conversion currently and REStake only send the Protobuf version. More importantly though, it doesn't seem like it's possible to send the right message structure based on the discussion in the below issues.

I need to spend a bit of time working out if it's possible to work around this, or if we have to wait for a chain upgrade to an unreleased SDK version.

Related:

cosmos/cosmjs#1026

cosmos/cosmos-sdk#11190

cosmos/cosmos-sdk#11224

batch messages into a single tx

in the cosmos-sdk a tx can contain many messages. A message is a claimreward or delegate message. This would allow for operators to have less fees per tx for now.

Bugs raised

Juno shows APY fetch warning even though disabled

Cosmos node is stalled - need to show latest block time + warning

Kava APY is off, needs disabling for now

Add Linting to Github CI

Since everyone is known to have a different way of programming, I suggest adding a linter to the Github CI. This way we get consistent code quality.

Allow local overrides without changing networks.json

Setup a networks.local.json file which is .gitignore'd but can be used to override any needed attributes from networks.json locally.

Would be useful for validators to add their own nodes so they don't rely on public for autocompounding. Also useful for overriding gas prices, adding extra networks. Whatever.

Easy to achieve with a deep merge too. Will sort ASAP

Related: #55 #44

Use feegrant for paying fees. Remove fee burden on the validators.

Fee-grant enables one to use fees from other accounts. Here instead of using hot wallet for fees, let delegators pay fees for their transactions. If there are 100k delegators, fees for a validator would be very high. It's a benefit for delegators, and competely optional for them. So they can pay fees

Question about WithdrawDelegatorReward

Really awesome tech being built here! ๐Ÿ™‚

Going thru the readme and i had a question about the following sentence.

REStake specifically lets you grant a validator permission to send WithdrawDelegatorReward and Delegate transactions for their validator only (note WithdrawDelegatorReward is technically not restricted to a single validator)

I'm very curious about the authority that users are handing over to validators by using restake so was wondering if someone add more clarity on the text in parenthesis? What are (if any) the implications of WithdrawDelegatorReward not being restricted to a single validator?

Automated tests

Generally need some unit tests, and ideally some kind of integration test too.

Also need validate networks.json submission. At least prefix etc. See #185

Nodes sometimes block certain paths

e.g. rest.stargaze-apis.com blocks the delegations lookup for a validator, which impacts autostake.

Because we load balance the nodes, these errors would come up intermittently.

Need to keep an eye, and potentially blacklist nodes from cosmos.directory as required.

Not updating data after redelegation

Some people have redelegated to validators through Keplr. In REStake, most of the chains shows this change but for the "Cosmos HUB" doesn't.

I've tried deleting the cache of my browser as sometimes it helps but it keeps showing that the delegation is still in the old validator. It also throws some errors if you try any other action through REStake.

P.D: I have not seen a similar issue related to this, sorry if this is a known issue

Show more validator information including runtime

Show countdown to runtime in delegations and validator listing
Show runtimes on validator profile
Show operator address, contact details, commission, rank on validator profile
Deep link to validator profile

Expire validator images

Currently validator image URLs are thrown into local storage keyed by the validator address. We have to query the keybase API to get the URL which is why we cache it like this.

Naturally.. these images will expire and local storage does not. I'm seeing a broken image for Frens and I suspect this is related. Need to store a bit of JSON instead including the image URL and a timestamp so we can expire images older than 24h(?).

Setup failed for desmos chain.

Might be a dump question. With the same mnemonic, I was able to set up the restake for osmosis chain, but was not able to set up on desmos chain.

By placing the corresponding osmo... address in the "botAddress" field in networks.json, It seems to give me a correct output:

Osmosis bot address is osmo145a2r0yurzkv43p2arhrwsqcy80pxg7guhadh2
Using REST URL https://lcd-osmosis.blockapsis.com
Using RPC URL https://osmosis.validator.network
You are using public nodes, script may fail with many delegations. Check the README to use your own
Running autostake
Bot balance is 0 uosmo
Bot balance is too low

But then when I did the same for desmos (of course using the corresponding desmos... address), it fails:

Desmos bot address is desmos145a2r0yurzkv43p2arhrwsqcy80pxg7gq5rdkq
Not an operator
Skipping

Any ideas why that would be the case? Thank you

Keplr Suggest Chain for unsupported chains

Keplr only supports a few chains out of the box. I get an error There is no chain info for cheqd-mainnet-1 for Cheqd for example. We need to use the Suggest Chain feature to hook this up. Info can come from Chain Registry, and cosmos.directory APIs.

Adding issue for my tracking but this will hold up #76 and others for now. @Galadrin I think we chatted about this too.

autostake script fails on cosmoshub due to api rate limits or rpc load

hi, thanks again for this!! what i encountered running the autostake script on cosmoshub:

scenario 1: RPC load makes connected node fall back

the script fails because at a point the node is not able to broadcast txs:

Found 7 delegators with valid grants...
cosmos... uatom reward is too low, skipping
cosmos... 892 uatom reward is too low, skipping
cosmos... 576 uatom reward is too low, skipping
cosmos... Autostaking 297743 uatom
cosmos... Successfully broadcasted
cosmos... 980 uatom reward is too low, skipping
cosmos... Autostaking 94897 uatom
cosmos... Successfully broadcasted
cosmos... Autostaking 14823 uatom
cosmos... Failed to broadcast: TimeoutError: Transaction with ID 3B1E27CF726851E2918ABEFB3D5FCAA4FF34F12AD35C618FB40F324AF7EA2E8D was submitted but was not yet found on the chain. You might want to check later.
    at pollForTx (/home/osmo/restake/node_modules/@cosmjs/stargate/build/stargateclient.js:213:23)
    at pollForTx (/home/osmo/restake/node_modules/@cosmjs/stargate/build/stargateclient.js:226:19)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  txId: '3B1E27CF726851E2918ABEFB3D5FCAA4FF34F12AD35C618FB40F324AF7EA2E8D'
}

this is common behaviour of heavy chain nodes, if RPC load is high the node will start lagging and fall out of sync, to catch up as soon as the load lowers significantly. We're experiencing this regularly on our relayer endpoints on various chains, but cosmoshub is the most prone to that error.

transactions "stuck" in this way will eventually be broadcasted to the mempool and confirm on-chain, but only if the node is able to reach the tip

proposed solution: batch txs (?); make broadcast TimeoutError non critical

workaround: repeatedly run script with chain argument until there are no errors anymore


scenario 2: LCD hits rate-limit:

the current connected public lcd hits some limit and times out:

path: '/cosmos/staking/v1beta1/validators/cosmosvaloper14qazscc80zgzx3m0m0aa30ths0p9hg8vdglqrc/delegations?pagination.limit=1000',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      host: 'lcd-cosmoshub.blockapsis.com',
      protocol: 'https:',
      _redirectable: [Writable],
      [Symbol(kCapture)]: false,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype]
    },
    data: '<html>\r\n' +
      '<head><title>504 Gateway Time-out</title></head>\r\n' +
      '<body>\r\n' +
      '<center><h1>504 Gateway Time-out</h1></center>\r\n' +
      '</body>\r\n' +
      '</html>\r\n'
  },
  isAxiosError: true

proposed solution: validators providing the service could use private REST endpoints without rate limits

while these errors turned up during tests on cosmoshub it doesn't mean they are limited to the hub, it can be assumed that the bigger the number of total delegators, the worse the load issues will be

Increase APY accuracy

Need to factor in actual block time.

if you have 6sec block time and 5sec theoric, you have 0.83% less APR than theory

you can calculate over 100 block to get ~ 1h mean
take time of latest heigh
take time of height - 100
divide per 100

=> you have the average block time

General documentation updates

Node requirements: indexing on, min gas prices, use your own node

Bit more info on key management for operators. Be clear about mnemonic being for hot wallet etc.

I'll add more as they come up

Sifchain integer issues

Sifchain has 18 decimals and Cosmjs seems to have issues processing integers, e.g. errors in console when delegating. Unsure if Sifchain is usable at all right now with REStake. Needs some more investigation.

Show voting power, rank and warnings

Include validator voting power/rank in UI and encourage delegators in the right direction to improve health of network.

I'm not sure if entirely disabling delegation functions is a good idea but could be some upper limits that make sense to do this

Improve networks.json

Doesn't merge well, don't know who the operators are at a glance.

Slightly improved now the chain-registry data is dynamic but could still be better.

Relates to #129

Evmos should be disabled

Accidentally re-enabled it when I switched to chain registry integration. Not a big deal but should probably be disabled for now.

Maybe should switch to explicit enabled: true option in networks.json, instead of disabled: true.

Error: Module not found

Hi, I am trying to become an operator of restake but I am not able to run the docker image successsfully.
I already have latest docker and docker-compose installed on an Ubuntu machine.
When I run sudo docker-compose run --rm app npm run autostake, the following error occurs:

`

[email protected] autostake
node scripts/autostake.mjs

node:internal/errors:465
ErrorCaptureStackTrace(err);
^

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@cosmjs/proto-signing' imported from /usr/src/app/scripts/autostake.mjs
at new NodeError (node:internal/errors:372:5)
at packageResolve (node:internal/modules/esm/resolve:907:9)
at moduleResolve (node:internal/modules/esm/resolve:956:20)
at defaultResolve (node:internal/modules/esm/resolve:1172:11)
at ESMLoader.resolve (node:internal/modules/esm/loader:580:30)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:294:18)
at ModuleWrap. (node:internal/modules/esm/module_job:80:40)
at link (node:internal/modules/esm/module_job:78:36) {
code: 'ERR_MODULE_NOT_FOUND'
}

Node.js v17.7.1
ERROR: 1
`
May I know how to resolve this problem? Thank you.

bug in 0.2.27

line 61 of /scripts/autostake.mjs:
let grantedAddresses = await this.getGrantedAddresses(addresses)
passes addresses as the only parameter, which doesn't match the function definition
on line 137 (same file):
async getGrantedAddresses(client, addresses){

Results in the following error:

file:///usr/src/app/scripts/autostake.mjs:138
    let grantCalls = addresses.map(item => {
                               ^

TypeError: Cannot read properties of undefined (reading 'map')
    at Autostake.getGrantedAddresses (file:///usr/src/app/scripts/autostake.mjs:138:32)
    at file:///usr/src/app/scripts/autostake.mjs:61:43
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Promise.all (index 0)
    at async executeSync (file:///usr/src/app/src/utils/Helpers.mjs:44:5)
    at async Autostake.run (file:///usr/src/app/scripts/autostake.mjs:76:5)

I'll work on a pull request to fix this later, just wanted to make people aware in the meantime.

Configurable default network

Currently the first network in the array is the default, i.e. redirecting from the root page.

Make this flexible, much like ownerAddress. Allows validators with their own UI to focus on their preferred network.

cc @gh0stdotexe @Galadrin

Add support for withdraw addresses

Add support for withdraw addresses

Upon detecting a withdraw address,

the grant for /cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward needs to come from the delegate address
the grant for AUTHORIZATION_TYPE_DELEGATE needs to come from the withdraw address.

So that means the user needs to sign from two different addresses ... I'm not sure how to make Keplr handle this gracefully.

Presumably the user with the withdraw address is slightly advanced and will be able to manage switching from one person to the other.

(Of course, this means the Revokes need to follow the same two address tango)

Get balance as required

Refresh balance after delegate, on schedule. Clear after network change before new network loads

autostake.mjs fails with new release

since this commit 237c3c1

file:///root/restake/scripts/autostake.mjs:113
    return client.queryClient.getBalance(client.operator.botAddress, client.network.denom)
                                                         ^

TypeError: Cannot read properties of undefined (reading 'botAddress')
    at Autostake.checkBalance (file:///root/restake/scripts/autostake.mjs:113:58)
    at file:///root/restake/scripts/autostake.mjs:47:20
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Promise.all (index 0)
    at async executeSync (file:///root/restake/src/utils/Helpers.mjs:44:5)
    at async Autostake.run (file:///root/restake/scripts/autostake.mjs:75:5)

osmosid autocomound error

Finding delegators...
Checking 313 delegators for grants...
...batch 1
...batch 2
ERROR: Error: connect ETIMEDOUT 54.183.249.236:443
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1158:16) {
errno: -110,
code: 'ETIMEDOUT',
syscall: 'connect',
address: '54.183.249.236',
port: 443,
config: {
transitional: {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false
},
adapter: [Function: httpAdapter],
transformRequest: [ [Function: transformRequest] ],
transformResponse: [ [Function: transformResponse] ],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
validateStatus: [Function: validateStatus],
headers: {
Accept: 'application/json, text/plain, /',
'User-Agent': 'axios/0.26.0'
},
method: 'get',
url: 'https://lcd-osmosis.blockapsis.com/cosmos/authz/v1beta1/grants?grantee=osmo194ytn50yhh67rdha8akhs7c6zulnz4n2pv6n63&granter=osmo159d5k2aaeepvxs3f7cv8uapeveh9a5shhu9amj',
data: undefined
},
request: <ref *1> Writable {
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: false,
decodeStrings: true,
defaultEncoding: 'utf8',
length: 0,
writing: false,
corked: 0,
sync: true,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: null,
writelen: 0,
afterWriteTickInfo: null,
buffered: [],
bufferedIndex: 0,
allBuffers: true,
allNoop: true,
pendingcb: 0,
constructed: true,
prefinished: false,
errorEmitted: false,
emitClose: true,
autoDestroy: true,
errored: null,
closed: false,
closeEmitted: false,
[Symbol(kOnFinished)]: []
},
_events: [Object: null prototype] {
response: [Function: handleResponse],
error: [Function: handleRequestError],
socket: [Function: handleRequestSocket]
},
_eventsCount: 3,
_maxListeners: undefined,
_options: {
maxRedirects: 21,
maxBodyLength: 10485760,
protocol: 'https:',
path: '/cosmos/authz/v1beta1/grants?grantee=osmo194ytn50yhh67rdha8akhs7c6zulnz4n2pv6n63&granter=osmo159d5k2aaeepvxs3f7cv8uapeveh9a5shhu9amj',
method: 'GET',
headers: [Object],
agent: undefined,
agents: [Object],
auth: undefined,
hostname: 'lcd-osmosis.blockapsis.com',
port: null,
nativeProtocols: [Object],
pathname: '/cosmos/authz/v1beta1/grants',
search: '?grantee=osmo194ytn50yhh67rdha8akhs7c6zulnz4n2pv6n63&granter=osmo159d5k2aaeepvxs3f7cv8uapeveh9a5shhu9amj'
},
_ended: true,
_ending: true,
_redirectCount: 0,
_redirects: [],
_requestBodyLength: 0,
_requestBodyBuffers: [],
_onNativeResponse: [Function (anonymous)],
_currentRequest: ClientRequest {
_events: [Object: null prototype],
_eventsCount: 7,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: false,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: 0,
_hasBody: true,
_trailer: '',
finished: true,
_headerSent: true,
_closed: false,
socket: [TLSSocket],
_header: 'GET /cosmos/authz/v1beta1/grants?grantee=osmo194ytn50yhh67rdha8akhs7c6zulnz4n2pv6n63&granter=osmo159d5k2aaeepvxs3f7cv8uapeveh9a5shhu9amj HTTP/1.1\r\n' +
'Accept: application/json, text/plain, /\r\n' +
'User-Agent: axios/0.26.0\r\n' +
'Host: lcd-osmosis.blockapsis.com\r\n' +
'Connection: close\r\n' +
'\r\n',
_keepAliveTimeout: 0,
_onPendingData: [Function: nop],
agent: [Agent],
socketPath: undefined,
method: 'GET',
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
path: '/cosmos/authz/v1beta1/grants?grantee=osmo194ytn50yhh67rdha8akhs7c6zulnz4n2pv6n63&granter=osmo159d5k2aaeepvxs3f7cv8uapeveh9a5shhu9amj',
_ended: false,
res: null,
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: 'lcd-osmosis.blockapsis.com',
protocol: 'https:',
_redirectable: [Circular *1],
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype]
},
_currentUrl: 'https://lcd-osmosis.blockapsis.com/cosmos/authz/v1beta1/grants?grantee=osmo194ytn50yhh67rdha8akhs7c6zulnz4n2pv6n63&granter=osmo159d5k2aaeepvxs3f7cv8uapeveh9a5shhu9amj',
[Symbol(kCapture)]: false
},
response: undefined,
isAxiosError: true,
toJSON: [Function: toJSON]
}

restake Pro (withdraw commission)

Mebbe add an optional Pro mode where a validator can grant restake

/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission

and then autostake can issue those....

Desmos Failed to broadcast: Unsupported type

Restake is able to pick up the list of supported validators and my own delegation, however when attempting any action (delegate, manual compound, claim, enable restake), the following error is thrown:

Failed to broadcast: Unsupported type: '/desmos.profiles.v1beta1.Profile'

Configurable autostake batch size

Currently limited to batch size of 100 for delegators and grant lookup. Reduced delegator batch size from 250 to 100 recently.

CosmosHub can still struggle with 100 batch size, and it really depends on node resources too. Might as well make this configurable per network so operators can override in their local networks json.

Allow multiple runtimes per day

Some validators will likely want to offer multiple runtimes per day. It could make sense for most validators to offer this if they set their minimum reward a bit higher.

Allow multiple runtimes, which feeds into the future APY feature (#80). Show full runtimes on the validator profile, and adapt the countdown to handle multiple times per day (#125)

Keplr address doesn't match autostaking script address

Had reports of this for Desmos, maybe others. When your autostaking mnemonic is imported into Keplr, the address matches for most networks but some it does not. E.g. Desmos.

For the purpose of running the script, the address spat out by npm run autostake is the correct one. This is the one you should fund and the script will send TXs from. I'm not sure how to actually access this in Keplr as of writing but it's probably derivation path related.

If you see this on any other networks, please add to this issue. I'll put a note in the README asap.

All promises rejected error

Hi mates,
My restake was working nicely for cosmoshub until today. I just pulled repo changes and rebuilt as usually.
What info would help to debug this?

This are the logs:

/usr/local/bin/docker-compose run --rm app npm run autostake cosmoshub
Creating restake_app_run ... done

> [email protected] autostake
> node scripts/autostake.mjs "cosmoshub"

[11:59:47.011] Cosmos Hub bot address is cosmos1dg8u5qll7fjm9nnwqns0ullhtc6g552ugd0eqj
[11:59:47.137] Failed to connect All promises were rejected

Before that it worked like this:

19-03-22/10:01:01
Creating restake_app_run ... 
Creating restake_app_run ... done

> [email protected] autostake
> node scripts/autostake.mjs "cosmoshub"

Cosmos Hub bot address is cosmos1dg8u5qll7fjm9nnwqns0ullhtc6g552ugd0eqj
Using REST URL http://127.0.0.1:1317
Using RPC URL http://127.0.0.1:26657
Running autostake
Bot balance is 72024 uatom
Finding delegators...
...batch 1
...batch 2
...batch 3
...batch 4
Checking 356 delegators for grants...
...batch 1
...batch 2
...batch 3
...batch 4

Autostaking script can batch up a lot more transactions

Currently we send one TX per delegator, which includes a single MsgExec TX with the two messages inside to withdraw and delegate.

There's no reason we can't send multiple MsgExec in a single transaction. Just need to work out how many is reasonable to include in a single batch

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.